Skip to main content
interlace
Plugin: nestjs-securityRules

no-res-bypass-serialization

Detect @Res() handlers that write objects past ClassSerializerInterceptor

Detect @Res() handlers that write objects past ClassSerializerInterceptor

This rule detects route handlers that inject @Res() without passthrough and then write an object, which silently ...

Rule Details

Injecting @Res() without passthrough: true switches a handler into library-specific mode. Nest stops handling the response, so no interceptor runs on it — and ClassSerializerInterceptor is an interceptor.

That means every @Exclude() on the object being written silently stops applying. The password hash that is stripped on every other route is serialized here, and nothing in the file says so.

This is NestJS-specific: it is not covered by any general-purpose security plugin, because the vulnerability is in the framework's response pipeline rather than in the code that writes the response.

OWASP Mapping

  • OWASP Top 10 2021: A01:2021 - Broken Access Control
  • CWE: CWE-200 - Exposure of Sensitive Information to an Unauthorized Actor
  • CVSS: 7.5 (High)

❌ Incorrect

@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string, @Res() res: Response) {
    // @Exclude() on UserEntity.password does NOT apply here.
    res.json(await this.usersService.findOne(id));
  }
}

✅ Correct

@Controller('users')
export class UsersController {
  // Interceptors still run, so @Exclude() still applies.
  @Get(':id')
  async findOne(
    @Param('id') id: string,
    @Res({ passthrough: true }) res: Response,
  ) {
    res.status(200);
    return this.usersService.findOne(id);
  }

  // Or drop @Res() entirely — the usual Nest handler.
  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

Options

{
  // Skip rule in test files (default: true)
  allowInTests?: boolean;
}

Scope

The bypass is only a disclosure risk when the handler writes an object. Narrowing to that took the finding count across ten measured codebases from 95 to 23 — the other 72 were file streams, redirects and status literals, where there is nothing to serialize.

Not reported:

res.sendFile(path); // streams a file
res.redirect(url); // no body
res.status(200).send('ok'); // a string literal cannot carry @Exclude()
return this.service.run(res); // res handed off — this file cannot follow it

When Not To Use It

  • If the project does not use ClassSerializerInterceptor or @Exclude() anywhere, the bypass has nothing to bypass.

Known False Negatives

Response passed to a service

Why: When res is handed to another function, what gets written to it lives in a different file.

// ❌ NOT DETECTED - the write happens elsewhere
@Get('callback')
callback(@Res() res: Response) {
  return this.authService.complete(res);
}

Mitigation: Prefer @Res({ passthrough: true }) as the project default.

Writes through an aliased reference

Why: Only writes on the injected binding are tracked.

// ❌ NOT DETECTED - rebound before the write
const out = res;
out.json(user);

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.