Summary
api/seed-contract-probe.ts authenticates the x-probe-secret header using JavaScript's !== operator rather than a timing-safe comparison. Every other auth check in the codebase uses crypto.subtle-based HMAC or constant-time comparison. This one outlier creates a timing oracle that can be used to recover the RELAY_SHARED_SECRET byte-by-byte.
Evidence
api/seed-contract-probe.ts lines ~225:
const secret = req.headers.get('x-probe-secret');
const expected = process.env.RELAY_SHARED_SECRET;
if (!expected) return jsonResponse({ error: 'not-configured' }, 503, cors);
if (secret !== expected) return jsonResponse({ error: 'unauthorized' }, 401, cors);
Contrast with the rest of the codebase (api/cache-purge.js, server/_shared/internal-auth.ts):
// Correct pattern used everywhere else:
const isValid = await timingSafeEqual(provided, expected);
Why this matters
RELAY_SHARED_SECRET is used across multiple internal services (Vercel functions, Railway relay, probe). Leaking this secret via timing oracle grants access to all auth-gated relay endpoints. The timing difference between an early string-compare exit (wrong first byte) and a full comparison (correct prefix) is measurable over a network with repeated sampling.
Attack or failure scenario
- Attacker sends requests to
/api/seed-contract-probe with varying x-probe-secret values.
- Measures response time distribution for each candidate byte at each position.
- Recovers the secret prefix statistically over ~1000 requests per byte.
- Uses recovered
RELAY_SHARED_SECRET to authenticate against all relay endpoints.
(This attack is practical when the relay is low-latency and the secret is short. It's less practical for long secrets on high-jitter connections, but the vulnerability is architectural.)
Root cause
Inconsistent security review across API endpoints. The timing-safe helper exists and is used elsewhere; this endpoint was not reviewed against the same standard.
Recommended fix
Replace the direct string comparison with the same timing-safe helper used throughout the rest of the codebase:
import { timingSafeEqual } from '../server/_shared/internal-auth.js';
const isValid = await timingSafeEqual(secret ?? '', expected);
if (!isValid) return jsonResponse({ error: 'unauthorized' }, 401, cors);
Acceptance criteria
Suggested labels
security, area: API
Priority
P2
Severity
Medium — timing oracle against a shared secret used across multiple services.
Confidence
Confirmed — contrast with api/cache-purge.js and server/_shared/internal-auth.ts verified.
Summary
api/seed-contract-probe.tsauthenticates thex-probe-secretheader using JavaScript's!==operator rather than a timing-safe comparison. Every other auth check in the codebase usescrypto.subtle-based HMAC or constant-time comparison. This one outlier creates a timing oracle that can be used to recover theRELAY_SHARED_SECRETbyte-by-byte.Evidence
api/seed-contract-probe.tslines ~225:Contrast with the rest of the codebase (
api/cache-purge.js,server/_shared/internal-auth.ts):Why this matters
RELAY_SHARED_SECRETis used across multiple internal services (Vercel functions, Railway relay, probe). Leaking this secret via timing oracle grants access to all auth-gated relay endpoints. The timing difference between an early string-compare exit (wrong first byte) and a full comparison (correct prefix) is measurable over a network with repeated sampling.Attack or failure scenario
/api/seed-contract-probewith varyingx-probe-secretvalues.RELAY_SHARED_SECRETto authenticate against all relay endpoints.(This attack is practical when the relay is low-latency and the secret is short. It's less practical for long secrets on high-jitter connections, but the vulnerability is architectural.)
Root cause
Inconsistent security review across API endpoints. The timing-safe helper exists and is used elsewhere; this endpoint was not reviewed against the same standard.
Recommended fix
Replace the direct string comparison with the same timing-safe helper used throughout the rest of the codebase:
Acceptance criteria
api/seed-contract-probe.tsuses a constant-time comparison for the probe secret.===or!==string comparison exists for any secret/token value across allapi/files.Suggested labels
security, area: API
Priority
P2
Severity
Medium — timing oracle against a shared secret used across multiple services.
Confidence
Confirmed — contrast with
api/cache-purge.jsandserver/_shared/internal-auth.tsverified.