Security rules

12 rules that detect security vulnerabilities and unsafe patterns.

RuleSeverityWhat it catches
no-hardcoded-secretserrorAPI keys, tokens, passwords in source code
no-evalerroreval() or new Function() usage
no-csrf-disablederrorcsrf: false or csrfProtection: false in an options object
no-dangerous-redirectserrorRedirects with user-controlled input
no-vulnerable-nestjs-packageserrorA watched package on a version with a critical or high advisory
no-synchronize-in-productionerrorsynchronize: true in TypeORM config
no-weak-cryptowarningcreateHash('md5') or createHash('sha1')
no-exposed-env-varswarningDirect process.env in Injectable/Controller
no-exposed-stack-tracewarningerror.stack exposed in responses
no-raw-entity-in-responsewarningHandler return type named *Entity or *Model
require-guards-on-endpointswarningEndpoints without @UseGuards() at class or method level
no-advisory-nestjs-packageswarningA 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:* or 11.x that 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-4q75

The 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 happenedMessage begins
A spec names no versionCould not establish the installed version of, then the packages
No package.json at or above the scanned pathFound no package.json at or above the scanned path
package.json is not valid JSONCould 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.