Skip to content

Add timestamp freshness validation to reject stale and future-dated location updates #174

Description

@levibliz

Background

The locationPayloadSchema in validator.js validates that timestamp is a valid ISO 8601 string when provided. However, it does not validate whether the timestamp is temporally reasonable. Clients can send timestamps from hours in the past or far in the future, corrupting historical tracks and bypassing replay-detection logic.

Category: Security / Enhancement
Difficulty: Medium
Priority: Medium
Labels: security, enhancement, backend

Problem Statement

Accepting arbitrarily old or future-dated timestamps enables:

  1. Data poisoning — Historical track records are polluted with spurious coordinates.
  2. Replay attacks — A captured valid message can be replayed indefinitely since there is no freshness check.
  3. Analytics corruption — Dashboards and geofence calculations may act on stale positions.

Requirements

  • Add a MAX_TIMESTAMP_SKEW_MS config value (default: 30000 — 30 seconds).
  • When timestamp is present in a location_update payload, reject the message if |Date.now() - Date.parse(timestamp)| > MAX_TIMESTAMP_SKEW_MS.
  • Return { ok: false, error: 'Timestamp is too old or too far in the future' }.
  • Log validation failures with logger.warn including clientId and the offending timestamp.
  • Add MAX_TIMESTAMP_SKEW_MS to .env.example.
  • Write unit tests: timestamp within window (accepted), too old (rejected), in future (rejected), no timestamp (accepted).

Acceptance Criteria

  • Messages with timestamps > 30 s in the past are rejected.
  • Messages with timestamps > 30 s in the future are rejected.
  • Messages with no timestamp field are accepted normally.
  • MAX_TIMESTAMP_SKEW_MS=0 rejects all messages that include any timestamp.
  • All new tests pass.

Implementation Notes

Implement the check inside validateMessage in validator.js after the Zod schema parse:

const skew = Number(process.env.MAX_TIMESTAMP_SKEW_MS ?? 30000);
if (result.data.type === 'location_update' && result.data.payload.timestamp) {
  const diff = Math.abs(Date.now() - Date.parse(result.data.payload.timestamp));
  if (diff > skew) return { ok: false, error: 'Timestamp is too old or too far in the future' };
}

Use vi.spyOn(Date, 'now') in tests to control the current time.

Files Likely to Change

  • src/validator.js
  • .env.example
  • tests/validator.test.js

Definition of Done

Stale and future-dated location updates are rejected. The check is configurable and fully tested.

Estimated Complexity

Medium — requires careful handling of the optional timestamp field and time-dependent testing strategy.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions