Security rules
12 rules that detect security vulnerabilities and unsafe patterns.
| Rule | Severity | What it catches |
|---|---|---|
no-hardcoded-secrets | error | API keys, tokens, passwords in source code |
no-eval | error | eval() or new Function() usage |
no-csrf-disabled | error | csrf: false or csrfProtection: false in an options object |
no-dangerous-redirects | error | Redirects with user-controlled input |
no-vulnerable-nestjs-packages | error | A watched package on a version with a critical or high advisory |
no-synchronize-in-production | error | synchronize: true in TypeORM config |
no-weak-crypto | warning | createHash('md5') or createHash('sha1') |
no-exposed-env-vars | warning | Direct process.env in Injectable/Controller |
no-exposed-stack-trace | warning | error.stack exposed in responses |
no-raw-entity-in-response | warning | Handler return type named *Entity or *Model |
require-guards-on-endpoints | warning | Endpoints without @UseGuards() at class or method level |
no-advisory-nestjs-packages | warning | A watched package on a version with a moderate or low advisory |
no-hardcoded-secrets
Detects API keys, tokens, passwords, and other secrets hardcoded in source code.
Why: Secrets in source code end up in version control, CI logs, and npm packages. Use environment variables instead.
@Injectable()
export class AuthService {
private readonly apiKey = 'sk-1234567890abcdef';
private readonly dbPassword = 'super_secret_password_2024';
}no-eval
Detects eval() and new Function() usage which can execute arbitrary code.
Why: Both eval() and new Function() can execute arbitrary strings as code, enabling code injection attacks.
@Injectable()
export class CalcService {
evaluate(expression: string) {
return eval(expression);
}
}no-csrf-disabled
Detects a csrf or csrfProtection property set to false.
Why: CSRF protection prevents cross-site request forgery attacks. Disabling it exposes your API to unauthorized actions from other sites.
Note: Those two property names are the only thing the rule reads, and the initializer has to be the literal false. A call like csurf({ cookie: false }) is not matched, because cookie is a different property.
// security.config.ts
export const securityConfig = {
csrf: false,
csrfProtection: false,
};no-dangerous-redirects
Detects a redirect whose argument is a parameter decorated with @Query() or @Param().
Why: Open redirects allow attackers to redirect users to malicious sites using your domain as a trusted intermediary.
Note: The match is on the argument itself, not on the code around it. Wrapping res.redirect(url) in an allowlist check still reports, because the argument is still the decorated parameter. Redirect to a different value instead, such as the result of a lookup:
@Get('redirect')
redirect(@Query('url') url: string, @Res() res: Response) {
res.redirect(url);
}The same match applies to @Redirect(), when its argument is the decorated parameter.
no-synchronize-in-production
Detects synchronize: true in TypeORM configuration.
Why: synchronize: true auto-alters database schema to match entities on every app start. This can drop columns and tables in production, causing data loss.
TypeOrmModule.forRoot({
type: 'postgres',
synchronize: true,
})no-weak-crypto
Detects usage of weak hashing algorithms (MD5, SHA-1).
Why: MD5 and SHA-1 are cryptographically broken. Use SHA-256 or stronger for any security-sensitive hashing.
import { createHash } from 'crypto';
const hash = createHash('md5').update(data).digest('hex');no-exposed-env-vars
Detects direct process.env access in Injectable or Controller classes.
Why: Direct process.env access distributes configuration reads across the codebase, complicates testing, and bypasses NestJS's ConfigService validation.
@Injectable()
export class MailService {
private readonly apiKey = process.env.MAIL_API_KEY;
}no-exposed-stack-trace
Detects error.stack being exposed in HTTP responses.
Why: Stack traces reveal internal file paths, dependency versions, and code structure to attackers.
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
response.json({ message: exception.message, stack: exception.stack });
}
}no-raw-entity-in-response
Detects controller handlers whose return type names an ORM entity.
Why: ORM entities may contain internal fields (passwords, soft-delete flags, relation metadata) that should not be exposed. Use DTOs to control the response shape.
Note: The entity is recognized by name. The return type has to contain a name ending in Entity or Model, so a handler returning Promise<User> is not matched. A type containing Dto, DTO, or Response is skipped.
The type comes from the checker, not from the text of the annotation. A handler with no annotation whose body infers any never matches, so annotate the return type to get the check.
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string): Promise<UserEntity> {
return this.userRepo.findOneOrFail(id); // Returns raw entity
}
}require-guards-on-endpoints
Detects controller endpoints that are not protected by @UseGuards() at the class or method level.
Why: Unguarded endpoints are accessible to anyone. Controllers or individual routes can opt out with @Public(), @AllowAnonymous(), @SkipAuth(), or @IsPublic() at the class or method level.
Note: A guard registered globally, either through APP_GUARD or with app.useGlobalGuards() in main.ts, already protects every endpoint, and the rule reports nothing for the whole project. The APP_GUARD token has to be written as APP_GUARD; an aliased import is not matched.
@Controller('orders')
export class OrderController {
@Get()
findAll() { // no guard
return this.orderService.findAll();
}
}no-vulnerable-nestjs-packages
Reports a watched package on a version with a critical or high advisory.
The watched set is every package published under the @nestjs scope, plus the
third-party packages a Nest project installs deliberately, such as
@sentry/nestjs and nestjs-pino. A package outside that set is never checked,
so the rule says nothing about it either way.
Why: an advisory is a defect with a public identifier and a known fix, so it belongs in the same pass as everything else rather than an audit nobody runs.
{
"devDependencies": {
"@nestjs/devtools-integration": "0.2.0"
}
}no-advisory-nestjs-packages
The same check for moderate and low advisories, reported as a warning.
Why: these are worth knowing about, and some have no fix in your current major, so they should not fail a build you were passing.
{
"dependencies": {
"@nestjs/core": "11.1.17"
}
}This rule declares surfaces: ["cli", "prComment"], so it reports in the console and on the pull request without moving the score or failing a build. The advisory list ships with the CLI, and a release that adds to it should not change how a project scores.
Which version is checked
Three tiers, in order:
- the version installed under
node_modules, which is what your code actually runs - otherwise the range, and only when every version it admits is below the fix
- otherwise the package is unchecked, for a spec like
workspace:*or11.xthat names no version
npm installs the highest version a range allows, so ^11.0.1 is quiet: it admits 11.1.18, which is patched. ^10.0.0 against a fix that exists only in 11.1.18 is reported, because no version it admits is patched.
The message names the spec as package.json declares it, so it matches the line the finding points at. A project declaring ^10.0.0 with 11.1.16 installed reports this:
⚠ @nestjs/core at ^10.0.0 is affected by CVE-2026-35515 (moderate): an unescaped newline in an SSE field lets an attacker spoof event types. Patched in 11.1.18. · not scored
Installed: 11.1.16. Upgrade to @nestjs/core@11.1.18 or newer. See https://github.com/advisories/GHSA-36xv-jgw5-4q75The resolved version sits in the help text below it. An exact spec is written with an @ instead, as @nestjs/core@11.1.17.
Nothing checked is still reported
A version that could not be established is a finding, not a silence. The same project reads as clean before an install and finds advisories after one, so the gap has to be visible.
| What happened | Message begins |
|---|---|
| A spec names no version | Could not establish the installed version of, then the packages |
No package.json at or above the scanned path | Found no package.json at or above the scanned path |
package.json is not valid JSON | Could not parse package.json |
All three come from no-advisory-nestjs-packages, so they reach the console and the pull request without moving the score. The search walks up from the scanned directory, so a sub-project with no manifest of its own still finds the workspace root's.
What it cannot do
The list ships with the CLI, so the check never queries an advisory service and the same project scores the same offline. The cost is freshness: an older install knows about fewer advisories, and npx nestjs-doctor@latest knows the most.
package.json takes no comments, so no inline directive reaches these findings. Silence one with ignore.rules in your config file.
Under --scope files, lines or changed, the finding appears only when package.json itself changed. changed is the scope the GitHub Action uses by default.
Scanning a subdirectory is the exception under files and lines. The rule walks up to the nearest package.json, and the changed-file set only describes the tree below it, so a finding above the scanned directory is kept. changed compares against the base revision, so it drops an advisory neither revision introduced.