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:
- Attacker manipulates device time to the future
- Device signs tokens with future
iattimestamps - Tokens are stored for later use
- Years later, the tokens become valid and can impersonate the device
Options
| Option | Type | Default | Description |
|---|---|---|---|
maxClockToleranceSeconds | number | 300 | Largest numeric clockTolerance (seconds) a verify may allow without a report |
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. |
{
"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 constWhat 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{ "jsPlugins": ["oxlint-plugin-jwt-security"], "rules": { "jwt-security/no-timestamp-manipulation": "error" }}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.