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
| Option | Type | Default | Description |
|---|---|---|---|
trustedSanitizers | string[] | [] | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
trustedAnnotations | string[] | [] | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
strictMode | boolean | false | No 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 castWhat 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 undefinedMitigation: 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{ "jsPlugins": ["oxlint-plugin-jwt-security"], "rules": { "jwt-security/require-max-age": "error" }}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.