Skip to main content
interlace
Plugin: jwtRules

require-max-age

This rule mandates maxAge in verify operations

Require maxAge option in verify operations to enforce token freshness

Severity: 🟡 Medium
CWE: CWE-294

Rule Details

This rule mandates maxAge in verify operations. Without it, tokens can be replayed years after issuance.

Examples

❌ Incorrect

jwt.verify(token, secret);
jwt.verify(token, secret, { algorithms: ['RS256'] });

✅ Correct

jwt.verify(token, secret, { maxAge: '1h' });
jwt.verify(token, secret, {
  algorithms: ['RS256'],
  maxAge: '24h',
});

// jose spells it maxTokenAge
await jwtVerify(token, JWKS, { algorithms: ['RS256'], maxTokenAge: '15m' });

clockTolerance does not satisfy this rule: it widens the exp / nbf window and caps nothing.

Options

OptionTypeDefaultDescription
trustedSanitizersstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
trustedAnnotationsstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
strictModebooleanfalseNo effect — accepted so existing configs stay valid; the rule does not read it yet.

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Options from Variables, Casts and Spreads

What is read: options are followed within the file — an inline object, a const or a never-reassigned let, a destructure, a member of an object literal, an as / satisfies cast, a spread of any of those, and the single return of a same-file function. NestJS JwtService calls are read at the second argument, where @nestjs/jwt takes them. An injected this.<member> counts as a JWT client only on evidence the class gives (a type annotation resolving to a JWT library, an @Inject(X) of a JWT import, or an assignment from one).

const verifyOptions = { algorithms: ['RS256'], maxAge: '15m' };
jwt.verify(token, key, verifyOptions); // ✅ read through the const
jwt.verify(token, key, { ...verifyOptions, complete: true }); // ✅ spread of a const
jwt.verify(token, key, { ...verifyOptions } as VerifyOptions); // ✅ through the cast

What is not: options from a parameter, an import, a function call, or a spread of one of those cannot be seen. The rule stays silent rather than report an option it cannot prove is missing. NestJS per-call options merge over JwtModule verifyOptions in another file, so they are treated the same way.

// ⚠️ NOT DETECTED - options built elsewhere
jwt.verify(token, key, getVerifyOptions());

Mitigation: Keep verify options in a module-level const in the file that verifies.

Spread of an Unresolvable Value

Why: { ...getDefaults() } may carry the option where this file cannot see it, so a missing option is not reported. A spread of a same-file const is read (see above).

// ⚠️ NOT DETECTED - the spread source is a call
jwt.verify(token, key, { ...getVerifyOptions() });

Runtime MaxAge Configuration

Why: MaxAge from runtime config is not visible.

// ❌ NOT DETECTED - MaxAge from config
jwt.verify(token, secret, { maxAge: config.tokenMaxAge }); // Might be undefined

Mitigation: Validate config at startup. Use required fields in TypeScript config types.

Excessive MaxAge Values

Why: Very large maxAge values (e.g., '100y') pass but are effectively non-enforcing.

// ❌ NOT DETECTED - Effectively no max age enforcement
jwt.verify(token, secret, { maxAge: '100y' });

Mitigation: Add option for maximum allowed maxAge value. Validate at runtime.

Wrapper Function

Why: Options passed through wrappers are not traced.

// ❌ NOT DETECTED - Wrapper hides options
function verifyToken(token: string, opts = {}) {
  return jwt.verify(token, secret, opts); // opts may lack maxAge
}

Mitigation: Apply this rule to all modules including utilities.

Further Reading

Use this rule

npm install -D oxlint-plugin-jwt-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-jwt-security"],  "rules": {    "jwt-security/require-max-age": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-jwt-security'],    rules: {      'jwt-security/require-max-age': 'error',    },  },});

oxlint-plugin-jwt-security re-exports eslint-plugin-jwt-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.