Skip to main content
interlace
Plugin: jwtRules

no-timestamp-manipulation

This rule detects noTimestamp: true which disables automatic iat (issued at) claim generation

Prevent disabling automatic timestamp generation which enables replay attacks

Severity: 🟠 High
CWE: CWE-294

Rule Details

This rule detects noTimestamp: true which disables automatic iat (issued at) claim generation. This enables the "Back to the Future" replay attack described in LightSEC 2025 research.

Examples

❌ Incorrect

// Disables iat - enables replay attacks
jwt.sign(payload, secret, { noTimestamp: true });

// Accepts expired tokens - a leaked token never stops working
jwt.verify(token, secret, { algorithms: ['HS256'], ignoreExpiration: true });
new JwtStrategy(
  { secretOrKey, jwtFromRequest, ignoreExpiration: true },
  verify,
); // passport-jwt

// A "clock tolerance" of a year keeps expired tokens alive for a year
jwt.verify(token, secret, {
  algorithms: ['HS256'],
  clockTolerance: 60 * 60 * 24 * 365,
});

✅ Correct

// Default behavior - iat is added
jwt.sign(payload, secret);
jwt.sign(payload, secret, { expiresIn: '1h' });

// Explicit false (redundant but clear)
jwt.sign(payload, secret, { noTimestamp: false });

// Seconds of clock skew, not hours
jwt.verify(token, secret, { algorithms: ['HS256'], clockTolerance: 30 });

clockTolerance is judged only when it is a number in every run: a literal, a same-file constant, or + - * / over those. jose's duration strings ('5 minutes') and values chosen at runtime are not reported.

The "Back to the Future" Attack

From LightSEC 2025 research:

  1. Attacker manipulates device time to the future
  2. Device signs tokens with future iat timestamps
  3. Tokens are stored for later use
  4. Years later, the tokens become valid and can impersonate the device

Options

OptionTypeDefaultDescription
maxClockToleranceSecondsnumber300Largest numeric clockTolerance (seconds) a verify may allow without a report
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.
{
  "jwt-security/no-timestamp-manipulation": ["error", {
    "maxClockToleranceSeconds": 300
  }]
}

Known False Negatives

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

Options from Variables, Casts and Spreads

What is read: an inline object, a same-file const, an as / satisfies cast, and a spread of such a const.

const opts = { noTimestamp: true };
jwt.sign(payload, secret, opts); // ❌ detected through the const

What is not: options from a parameter, an import or a call.

// ⚠️ NOT DETECTED - options built elsewhere
jwt.sign(payload, secret, getSignOptions());

Spread of an Unresolvable Value

// ⚠️ NOT DETECTED - the spread source is a call
jwt.sign(payload, secret, { ...getSignOptions() });

Dynamic Boolean Values

Why: Boolean computed at runtime is not evaluated.

// ❌ NOT DETECTED - Dynamic boolean
const disableTimestamp = getConfig().disableIat; // Returns true
jwt.sign(payload, secret, { noTimestamp: disableTimestamp });

Mitigation: Use TypeScript literal types. Validate config at startup.

Cross-Module Options

Why: Options imported from other modules are not traced.

// ❌ NOT DETECTED - Options from import
import { JWT_OPTIONS } from './config'; // { noTimestamp: true }
jwt.sign(payload, secret, JWT_OPTIONS);

Mitigation: Apply this rule to all modules including config files.

Further Reading

Use this rule

npm install -D oxlint-plugin-jwt-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-jwt-security"],  "rules": {    "jwt-security/no-timestamp-manipulation": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-jwt-security'],    rules: {      'jwt-security/no-timestamp-manipulation': '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.