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 itWhen Not To Use It
- If the project does not use
ClassSerializerInterceptoror@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.