Threat Model
This document is the canonical threat model for atemporal. It complements the disclosure process described in the security policy and the security best-practices page in the cookbook.
Asset inventory
atemporal is a client-side date/time library. The "assets" it protects are:
- Caller time and correctness. A user-visible wrong date (off-by-one, wrong timezone) is a silent bug that can corrupt audit logs, schedules, and analytics.
- Caller process stability. A
RangeErrorthrown out of a regex backtracking loop, or aMaximum call stackfrom a recursive parser, can take down a Node process. - Caller error-handling surface. If atemporal throws an error that cannot be matched by
instanceof, the caller cannot translate it to a localized message.
Adversary model
atemporal assumes:
- Inputs can be malicious. Strings, numbers,
Dateobjects, and plain objects come from untrusted sources (HTTP body, URL parameter, queue message, third-party webhook). - The host environment is benign. The browser, Node.js, or Worker runtime is not compromised. (If it is, atemporal cannot help — your process is already owned.)
- Plugins are trusted. Plugins are loaded from npm packages the consumer has chosen. Plugins run with full access to the
TemporalWrapperclass.
atemporal does not assume:
- That inputs are valid. The library must produce a defined behaviour for any input.
- That the caller's validator has run. atemporal is the second line of defense.
In-scope threats
T1: Regular expression denial of service (ReDoS)
Description: an attacker submits a long, structured string that causes a regex in customParseFormat to backtrack catastrophically.
Mitigation:
- All format-token regexes are linear in input length.
- We do not use nested quantifiers (
(a+)+,(a*)*). - The string strategy enforces a default 4 096-character input cap (configurable via
setMaxInputLength). - The CI fuzz tests run 1 000 random strings per build (see
__tests__/atemporal.fuzz.test.ts).
Residual risk: a regression that introduces a new regex without a code review will not be caught by automated tools. The threat model is enforced by the code review checklist, not by tests.
T2: Prototype pollution via plain-object input
Description: an attacker submits { "__proto__": { "polluted": 1 } } as if it were a plain date object. The parser might assign obj.year = 2024, accidentally polluting Object.prototype.
Mitigation:
- The plain-object strategy only ever reads known keys (
year,month,day,hour,minute,second,millisecond,timeZone,calendar). - We never spread (
{...obj}) or assign to keys we did not destructure. - The fuzz test
does not get confused by prototype pollution attemptsexplicitly checks for this.
Residual risk: none known. Verified by fuzzing.
T3: Cross-implementation instanceof confusion
Description: a project loads both @js-temporal/polyfill and the native Temporal API. A value from one fails instanceof checks in the other. The parser would silently return the wrong answer.
Mitigation:
- All internal code goes through
getCachedTemporalAPI()insrc/core/temporal-api.ts, which is initialized once per process. - The polyfill import is
import * as Temporal from '@js-temporal/polyfill'(a single module); the native API is the globalTemporalobject. - We never mix the two in a single function.
Residual risk: if a consumer manually constructs a value with the "wrong" implementation, our parser will throw. We have a clear error message but no automatic recovery.
T4: Numeric edge cases
Description: an attacker submits Number.MAX_SAFE_INTEGER, Number.MAX_VALUE, NaN, or Infinity.
Mitigation:
Number.isFinite()andNumber.isInteger()guards are applied at every numeric parse path.NaNpropagates to a.isValid() === falseresult.Infinityreturns an invalid wrapper (no crash).
Residual risk: none. The fuzz tests cover these cases.
T5: Format string injection
Description: an attacker submits a user-controlled string as a format (not a date) — e.g. atemporal(input, fmt).format(fmt). The format contains tokens that could read state or do something harmful.
Mitigation:
- The formatting engine is a pure function: it reads
.year,.month, etc. and emits tokens. It does not eval, do string interpolation, or read user data. - All user-controlled values must be inserted via
[literal]escapes, which are explicitly safe.
Residual risk: none.
T6: Locale and timezone DOS
Description: an attacker submits an IANA timezone string that the polyfill tries to load, taking seconds or hanging.
Mitigation:
Intl.DateTimeFormatis initialized once per timezone+locale and cached inIntlCache. The second call returns the cachedIntl.DateTimeFormatsynchronously.- A bad IANA name throws
InvalidTimeZoneErrorimmediately (becauseIntl.DateTimeFormatvalidates it).
Residual risk: none known. The CI tests run on America/New_York, Europe/London, Asia/Tokyo, etc.
T7: Information disclosure via error messages
Description: an error message leaks internal state (file paths, env vars, secrets).
Mitigation:
- User-facing error messages are static and audited.
debugLognever logs rawErrorobjects or stack traces.- Errors do not include the original input by default (some include it; see
InvalidFormatError.formatString,FormatMismatchError.dateString— these are intentional but bounded).
Residual risk: low. We rely on code review to catch new error messages that include user data.
Out-of-scope
- Bugs in
@js-temporal/polyfillitself. - Denial-of-service through deliberate resource exhaustion (e.g. parsing a million strings in a tight loop).
- Vulnerabilities in user code that uses atemporal.
- Side-channel attacks (e.g. timing differences) — atemporal's parse times are input-dependent, but the variance is bounded.
Reporting a new threat
If you find a new attack vector that is not in the list above, please open a private security advisory as described in the security policy.
We aim to update this document every time a new threat class is discovered or a mitigation lands.
Change log
- 2026-06-03 — Initial document (Sprint 2.4 of the audit).