no-permissive-cors
Flags CORS configured to accept any origin — a bare enableCors(), origin '*', or the reflecting origin true.
Disallows CORS configured to accept any origin
Flags CORS configured to accept any origin — a bare enableCors(), origin: '*', or the reflecting origin: true.
Rule Details
Three shapes all accept every origin, and only one of them looks obviously wrong:
app.enableCors(); // no options → origin defaults to '*'
app.enableCors({ origin: '*' }); // explicit wildcard
app.enableCors({ origin: true }); // reflects whatever Origin was sentorigin: true is the one worth understanding. It does not mean "CORS on" — it
echoes the request's own Origin header straight back in
Access-Control-Allow-Origin, so every site passes the check. Unlike '*', it
also stays valid alongside credentials: true, and browsers will send
cookies on those requests. That combination lets any page your user visits read
authenticated responses from your API.
Measured on five real NestJS applications, both CORS call sites present were
permissive: one bare enableCors() and one enableCors({ origin: true }).
❌ Incorrect
app.enableCors();
app.enableCors({ credentials: true }); // no origin key → default '*' applies
app.enableCors({ origin: '*' });
app.enableCors({ origin: true, credentials: true });✅ Correct
app.enableCors({ origin: ['https://app.example.com'] });
app.enableCors({ origin: 'https://app.example.com' });
app.enableCors({ origin: false }); // CORS explicitly off
// Resolved at runtime — the rule does not guess at these
app.enableCors({ origin: configService.get('cors.origin') });
app.enableCors({ origin: (o, cb) => cb(null, allowed.includes(o)) });What this rule deliberately does not report
A security rule that guesses earns a reputation for noise, so anything not statically decidable is left alone:
- an
originthat is a variable, member expression, template literal with something interpolated, array, regex, or callback (`*`with nothing interpolated is the string'*'and is reported) - an options object imported from another module —
enableCors(corsOptions)only reports whencorsOptionsis declared in the same file - an options object with a spread (
{ ...base }) and no visibleorigin, since the spread may supply it - a variable that is reassigned after its declaration. The value at the call site may not be the value at the declaration, so only a binding written exactly once — its initialiser — is read.
Environment checks
A permissive call that can only run outside production is not reported. A branch counts when both hold:
- its condition mentions one of
environmentHintNamesas a whole identifier or word (NODE_ENV,isDev,'development', …); and - read in the branch's direction, the condition selects a development
environment. A condition naming
production/prod/isProdselects production, or development when negated (!==,!=, a leading!); one namingdevelopment/dev/test/local/staging/isDev/devModeselects development, or production when negated.
The if / ?: consequent and the right of && run when the condition is
true; the else / : alternate and the right of || run when it is false.
if (process.env.NODE_ENV !== 'production') app.enableCors({ origin: '*' }); // not reported
isDev && app.enableCors({ origin: true }); // not reported
if (process.env.NODE_ENV === 'production') {
} else {
app.enableCors({ origin: '*' }); // not reported: runs outside production
}
if (process.env.NODE_ENV === 'development') {
} else {
app.enableCors({ origin: '*' }); // reported: this branch is production
}
if (req.path.startsWith('/public')) app.enableCors({ origin: '*' }); // reported: not about the environmentenvironmentHintNames replaces the default list. A project whose flag is
stage sets ['stage'], after which if (stage === 'local') counts — and
NODE_ENV no longer does unless it is listed too.
The call is enableCors on any receiver however the member is spelled:
app.enableCors(…) and app['enableCors'](…) are the same call.
Options
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | true | Skip this rule in *.test.* / *.spec.* files |
environmentHintNames | string[] | ["NODE_ENV","APP_ENV","ENVIRONMENT","isDev","isDevelopment","isLocal","isTest","isProd","isProduction","devMode","development","production"] | Words that mark a condition as an environment check. Replaces the default. |
'nestjs-security/no-permissive-cors': ['error', { allowInTests: true }]| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | true | Skip *.spec.ts / *.test.ts / *.e2e-spec.ts |
When Not To Use It
If the service is a genuinely public, unauthenticated, read-only API where any
origin reading responses is intended. Even then, prefer origin: '*' written
explicitly over a bare enableCors(), so the intent is visible to the next
reader.
Further Reading
Use this rule
npm install -D oxlint-plugin-nestjs-security{ "jsPlugins": ["oxlint-plugin-nestjs-security"], "rules": { "nestjs-security/no-permissive-cors": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-nestjs-security'], rules: { 'nestjs-security/no-permissive-cors': 'error', }, },});oxlint-plugin-nestjs-security re-exports eslint-plugin-nestjs-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
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.