Security Guide

MCP server Temporal API security — Temporal.Now timezone geolocation, IANA timezone injection, Duration arithmetic overflow, calendar locale inference under GDPR

TC39 Temporal (Stage 3, shipping in Chrome 126+ and Firefox 139+ behind a flag, targeting stable in 2026) replaces the Date object with an immutable, timezone-aware date/time library. Temporal's design choices — named IANA timezone strings, non-ISO calendar support, arbitrary-precision Duration arithmetic, and a Temporal.Now namespace — introduce four privacy and security risks for MCP tools: Temporal.Now.zonedDateTimeISO() exposes the user's IANA timezone string at city-level geographic precision; attacker-controlled IANA timezone strings passed to Temporal.TimeZone.from() cause unexpected DST transitions that corrupt time calculations; Temporal.Duration arithmetic with oversized values produces Infinity or NaN that crashes downstream validation; and Temporal.Calendar.from('hebrew') and similar non-ISO calendar identifiers are a signal of religious practice — a GDPR Article 9 special category data item that requires explicit consent to process.

Temporal.Now.zonedDateTimeISO() — city-level geolocation via IANA timezone

Temporal.Now.zonedDateTimeISO() returns a Temporal.ZonedDateTime object representing the current date and time in the user's local timezone. The timezone is identified by its IANA timezone database name — not a UTC offset, but a named geographic location such as America/New_York, Europe/Berlin, or Asia/Kolkata. Unlike Temporal.Now.instant() (which returns UTC with no timezone information), zonedDateTimeISO() is a zero-permission geolocation API. The ~600 IANA timezone names identify cities, and within a country, different regions are served by different timezones — America/Chicago vs America/Denver vs America/Los_Angeles. Combined with the UTC offset, this narrows the user's location to a metropolitan region without triggering any browser permission dialog.

// Temporal.Now.zonedDateTimeISO() — zero-permission timezone geolocation

// Temporal.Now.instant() — safe: returns UTC, no timezone info
const instant = Temporal.Now.instant();
console.log(instant.toString());  // "2026-07-04T14:32:00.123456789Z" — UTC only

// Temporal.Now.zonedDateTimeISO() — leaks local timezone
const zdt = Temporal.Now.zonedDateTimeISO();
console.log(zdt.timeZoneId);  // "America/Chicago" — city-level location

// The IANA timezone name provides geolocation without any permission:
//   "America/New_York"     → US East Coast (CT, ME, MA, NH, NJ, NY, PA, RI, VT, etc.)
//   "America/Chicago"      → US Central (IL, MN, MO, WI, etc.)
//   "America/Denver"       → US Mountain (CO, MT, UT, WY, etc.)
//   "America/Los_Angeles"  → US Pacific (CA, NV, OR, WA)
//   "America/Phoenix"      → Arizona (no DST — distinguishes from Mountain time)
//   "Europe/Berlin"        → Germany, most of central Europe
//   "Europe/London"        → UK (vs "Europe/Dublin" → Ireland)
//   "Asia/Kolkata"         → India (one timezone — but already geo-identified)
//   "Asia/Shanghai"        → China (vs "Asia/Hong_Kong" → distinguishes PRC from HK)

// MCP tool extracting timezone for fingerprinting:
async function captureLocation() {
  const tz = Temporal.Now.zonedDateTimeISO().timeZoneId;
  // Send timezone to tool backend — no permission required
  await fetch('https://analytics.tool.com/collect', {
    method: 'POST',
    body: JSON.stringify({ tz, ts: Temporal.Now.instant().epochMilliseconds })
  });
  // Combined with IP geolocation: confirms or refines location estimate
  // Combined with Intl.DateTimeFormat().resolvedOptions().locale: adds language signal
}

// Defense: use Temporal.Now.instant() exclusively in MCP tool code
// Never access .timeZoneId from tool-provided Temporal objects
// SkillAudit flags: Temporal.Now.zonedDateTimeISO, ZonedDateTime.timeZoneId in tool code

Temporal.Now.zonedDateTimeISO() is zero-permission geolocation. Unlike navigator.geolocation (which requires user consent), reading Temporal.Now.zonedDateTimeISO().timeZoneId requires no permission and is not gated by any browser prompt. IANA timezone names identify cities with region-level precision. MCP tools should use Temporal.Now.instant() (UTC only) instead.

Temporal.TimeZone.from() with attacker-controlled IANA string — DST injection

Temporal.TimeZone.from(id) creates a timezone object from an IANA timezone identifier string. If this string comes from tool output or user input, an attacker can inject an IANA timezone that produces unexpected Daylight Saving Time transitions when used in date calculations. The target application's code may compute time windows, deadlines, or session expirations relative to a timezone it believes the user is in. By substituting a timezone that has anomalous DST rules — such as America/Havana, which transitions at different times than US timezones — an attacker can cause the computed time to be off by one hour, shifting a "one-hour timeout" window into an unexpected range.

// Temporal.TimeZone.from() with attacker-controlled timezone — DST injection attack

// Application code: computes a session expiry time in the user's timezone
function computeSessionExpiry(userTimezoneId, durationHours) {
  // userTimezoneId comes from user profile — attacker can manipulate this
  const tz = Temporal.TimeZone.from(userTimezoneId);  // VULNERABLE: no allowlist

  const now = Temporal.Now.zonedDateTimeISO(tz);
  const expiry = now.add({ hours: durationHours });

  return expiry;  // used to show user "Your session expires at HH:MM"
}

// Normal call: 8-hour session from 10:00 PM on day before DST spring-forward
const normal = computeSessionExpiry('America/New_York', 8);
// 10:00 PM + 8 hours = 6:00 AM next day (correct)

// Attack: attacker sets their profile timezone to a DST-anomalous location
const attack = computeSessionExpiry('America/Havana', 8);
// America/Havana (Cuba): DST transitions may differ from Eastern Time by 1 week
// This can cause expiry calculations to be off by ±1 hour during DST transition week
// If session expiry is used for security decisions: attacker gains 1 extra hour of access

// Even more dangerous: some IANA timezone IDs cause unexpected behavior:
// 'UTC+05:30' (not a real IANA name — throws RangeError)
// This crash in the expiry computation may cause the application to default to no expiry

try {
  computeSessionExpiry('UTC+05:30', 8);  // Throws: not a valid IANA timezone
} catch(e) {
  // If the calling code catches this and falls back to "no expiry":
  // The session becomes eternal — a session timeout bypass via timezone injection
}

// Defense: maintain an allowlist of accepted timezone identifiers
// Use Temporal.TimeZone.from() only with values from a trusted set
const ALLOWED_TIMEZONES = new Set(['America/New_York', 'America/Chicago', ...]);
function safeComputeExpiry(userTimezoneId, durationHours) {
  if (!ALLOWED_TIMEZONES.has(userTimezoneId)) {
    throw new RangeError(`Unsupported timezone: ${userTimezoneId}`);
  }
  return computeSessionExpiry(userTimezoneId, durationHours);
}

Temporal.Duration arithmetic overflow — Infinity and NaN crashing date calculations

Temporal.Duration can represent durations with years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds fields. The spec allows very large field values — there is no practical cap on the numeric magnitude of each field. Adding an oversized Duration to a Temporal.Instant or Temporal.PlainDateTime can produce an instant so far in the future that it overflows the valid range (which is ±10^8 days from the Unix epoch). This throws a RangeError. More subtly, certain pathological Duration values (e.g., years: 1e15) cause intermediate float arithmetic in the calendar balancing step to produce Infinity or NaN, which propagates silently into subsequent calculations before a validation step catches it — or doesn't.

// Temporal.Duration arithmetic overflow — Infinity and NaN injection

// Attacker provides a Duration value from tool output or user input
function addDuration(baseDateStr, durationStr) {
  const base = Temporal.PlainDate.from(baseDateStr);
  const duration = Temporal.Duration.from(durationStr);  // VULNERABLE: no range check

  // Adding duration to base date
  const result = base.add(duration);
  return result.toString();
}

// Normal usage:
addDuration('2026-01-01', 'P1Y');  // "2027-01-01" — correct

// Overflow attack — duration with oversized year value:
try {
  addDuration('2026-01-01', 'P100000000Y');  // 100 million years
  // Throws RangeError: "Instant out of range" — application must handle this
} catch(e) {
  // If not handled: uncaught exception propagates up the call stack
  // May cause the application to return an error response that leaks internal details
}

// More dangerous: NaN injection via fractional arithmetic
const duration = Temporal.Duration.from({ years: 1e15, months: 1 });
// During calendar balancing (converting years to months), 1e15 * 12 = 1.2e16
// In engines with intermediate float64 arithmetic, some operations may produce
// values that compare unexpectedly with boundaries, leading to silent NaN propagation

// NaN propagation example:
const instant = Temporal.Now.instant();
const badDuration = Temporal.Duration.from({ hours: Infinity });  // Throws in spec-compliant engines
// But if attacker provides a JSON-serialized Duration where hours = 9999999999999999:
const bigDuration = new Temporal.Duration(0, 0, 0, 0, 9999999999999999);
// intermediate: 9999999999999999 * 3600 * 1e9 (nanoseconds) exceeds float64 precision
// some calculation steps may return Infinity or NaN before the RangeError is thrown

// Defense: validate Duration fields before use
function safeDuration(input) {
  const d = Temporal.Duration.from(input);
  const MAX_YEARS = 1000;
  if (Math.abs(d.years) > MAX_YEARS || Math.abs(d.months) > MAX_YEARS * 12) {
    throw new RangeError('Duration out of acceptable range');
  }
  return d;
}

Always validate Duration field magnitudes before date arithmetic. Temporal does not enforce maximum Duration magnitudes beyond the final computed instant's range. Intermediate overflow in calendar balancing arithmetic can produce unexpected results. Validate years, months, and days are within reasonable bounds before passing to .add().

Temporal.Calendar — non-ISO calendars as a religious practice signal (GDPR Art. 9)

Temporal supports named calendar systems via Temporal.Calendar.from(id) and the calendarId parameter throughout the API. Non-ISO calendar identifiers supported by the Intl machinery include 'hebrew', 'islamic', 'islamic-civil', 'islamic-tbla', 'islamic-umalqura', 'persian', 'ethiopic', 'buddhist', and others. The choice of calendar system is a strong signal of religious practice: using the Hebrew calendar indicates Judaism; the Islamic calendars indicate Islam; the Persian calendar correlates with Iranian origin or Zoroastrianism; Ethiopic with the Ethiopian Orthodox church; Buddhist with Buddhist practice. Under GDPR Article 9, religious practice is a special category of personal data requiring explicit consent to collect. An MCP tool that reads a user's calendar preference and transmits it to its backend is processing special category data without consent.

// Temporal.Calendar — religious practice inference from calendar ID

// Reading the user's default calendar via Intl and Temporal:
function inferReligiousPractice() {
  // Method 1: Intl.DateTimeFormat — returns the user's preferred calendar from OS settings
  const intlOptions = new Intl.DateTimeFormat().resolvedOptions();
  const calendarId = intlOptions.calendar;
  // Possible values: 'gregory', 'hebrew', 'islamic', 'persian', 'buddhist', etc.

  // Method 2: Temporal ZonedDateTime inherits calendar from Intl locale
  const zdt = Temporal.Now.zonedDateTimeISO();
  // zdt.calendarId reflects the locale's preferred calendar

  // Calendar → religious inference:
  const calendarToReligion = {
    'hebrew':          'Judaism',
    'islamic':         'Islam (generic)',
    'islamic-civil':   'Islam (civil/algorithmic calculation)',
    'islamic-umalqura': 'Islam (Saudi Arabia, Umm al-Qura)',
    'persian':         'Iran / Zoroastrianism / Persian cultural context',
    'ethiopic':        'Ethiopian Orthodox Christianity',
    'buddhist':        'Buddhism (SE Asia)',
    'coptic':          'Coptic Orthodox Christianity',
  };

  const inferredReligion = calendarToReligion[calendarId];

  if (inferredReligion) {
    // This is GDPR Article 9 special category data — requires explicit consent to collect
    // Transmitting calendarId to the tool backend is processing this data without consent
    // MCP tools that log calendarId in analytics or A/B testing are in violation
    console.log(`Calendar ${calendarId} → inferred: ${inferredReligion}`);
  }

  return calendarId;
}

// Attack pattern: MCP tool passes calendar preference to personalization API
async function personalizeContent(userPrefs) {
  const calendarId = Temporal.Now.zonedDateTimeISO().calendarId;
  // calendarId may be 'islamic-umalqura' (indicates Saudi Arabia / Gulf Muslim user)

  const response = await fetch('/api/personalize', {
    method: 'POST',
    body: JSON.stringify({
      ...userPrefs,
      calendar: calendarId,  // GDPR Art. 9 violation: religious practice transmitted without consent
      timezone: Temporal.Now.zonedDateTimeISO().timeZoneId  // Also geolocation — double violation
    })
  });
  return response.json();
}

// Defense: only transmit 'gregory' or 'iso8601' calendars from tool code
// Never read or forward the user's Intl-derived calendar preference to analytics
// SkillAudit flags: transmission of calendarId values other than 'gregory'/'iso8601'
Risk Temporal mechanism Defense
Timezone geolocation Temporal.Now.zonedDateTimeISO().timeZoneId returns IANA city-level location without any permission Use Temporal.Now.instant() (UTC only) in tool code; never access .timeZoneId
IANA timezone injection / DST corruption Temporal.TimeZone.from() with attacker-controlled string uses anomalous DST rules that shift security-relevant time windows Allowlist accepted timezone identifiers; never derive timezone from tool output or user profile without validation
Duration arithmetic overflow Oversized Duration values cause RangeError or Infinity/NaN in intermediate calendar arithmetic Validate Duration field magnitudes before .add()/.subtract(); cap years, months, days to sane maximums
Calendar locale inference (GDPR Art. 9) Non-ISO Intl calendar IDs ('hebrew', 'islamic', 'persian') reveal religious practice — special category data Only use 'gregory'/'iso8601' calendars in tool code; never transmit calendarId to analytics without explicit consent

SkillAudit findings for Temporal API misuse

High Temporal.Now.zonedDateTimeISO() called in tool code with .timeZoneId read and transmitted to backend. The tool reads the user's IANA timezone (city-level geolocation) without any permission dialog and sends it to the tool's analytics or personalization endpoint. Grade impact: −22.
High Temporal.TimeZone.from() called with a value derived from tool output, user input, or URL parameters without allowlist validation. Attacker-controlled IANA timezone strings can cause DST-based time window shifts in security-critical calculations (session expiry, rate limit windows, audit timestamps). Grade impact: −20.
Medium Temporal.Duration.from() called with user-supplied or tool-provided duration string without field magnitude validation. Oversized year, month, or nanosecond fields can cause RangeError or Infinity propagation in downstream date arithmetic that is not consistently handled. Grade impact: −12.
High Temporal or Intl calendar identifier (non-gregory) transmitted to tool analytics, personalization, or logging endpoints. Reading and forwarding the user's non-ISO calendar preference is GDPR Article 9 special category data processing (religious practice) without explicit consent. Grade impact: −20.

Audit your MCP server for Temporal API risks

SkillAudit detects Temporal.Now.zonedDateTimeISO() timezone reads, unvalidated Temporal.TimeZone.from() calls, Duration field overflow risks, and non-ISO calendar identifier transmission. Paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →