Add request ID validation and CSPRNG fallback (FRE-4516)

- Max-length guard (256 chars) on incoming request IDs to prevent log bloat
- Format whitelist (alphanumeric, hyphen, underscore) to prevent log injection
- Replace Math.random() with crypto.randomBytes in fallback for CSPRNG
This commit is contained in:
2026-05-02 09:43:13 -04:00
parent fe754761d9
commit 8687868632

View File

@@ -1,36 +1,60 @@
import nodeCrypto from "crypto";
const MAX_REQUEST_ID_LENGTH = 256;
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
/**
* Validates a request ID for safe logging: enforces max length
* and whitelist of safe characters to prevent log injection.
*/
function isValidRequestId(id: string): boolean {
if (id.length > MAX_REQUEST_ID_LENGTH) return false;
if (!REQUEST_ID_PATTERN.test(id)) return false;
return true;
}
/** /**
* Generates a unique request ID for distributed tracing. * Generates a unique request ID for distributed tracing.
* Uses crypto.randomUUID when available, falls back to a * Uses crypto.randomUUID when available, falls back to a
* timestamp-based UUID v4 format. * Node.js crypto.randomBytes-based UUID v4 format.
*/ */
export function generateRequestId(): string { export function generateRequestId(): string {
if (typeof crypto !== "undefined" && crypto.randomUUID) { if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID(); return crypto.randomUUID();
} }
const bytes = nodeCrypto.randomBytes(16);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = bytes.toString("hex");
return ( return (
[0, 0, 0, 0, 0].map((_, i) => { hex.slice(0, 8) + "-" +
const segment = i === 3 ? 8 : 4; hex.slice(8, 12) + "-" +
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) hex.slice(12, 16) + "-" +
.toString(16) hex.slice(16, 20) + "-" +
.padEnd(i === 4 ? 12 : 4, "0") hex.slice(20, 32)
.slice(0, i === 4 ? 12 : 4);
}).join("-")
); );
} }
/** /**
* Extracts an existing request ID from headers or generates a new one. * Extracts an existing request ID from headers or generates a new one.
* Checks standard headers: X-Request-Id, X-Correlation-Id, X-Trace-Id. * Checks standard headers: X-Request-Id, X-Correlation-Id, X-Trace-Id.
* Validates extracted IDs for safe logging (max 256 chars, alphanumeric/hyphen/underscore).
*/ */
export function extractOrGenerateRequestId(headers: Record<string, string | string[] | undefined>): string { export function extractOrGenerateRequestId(headers: Record<string, string | string[] | undefined>): string {
const candidates = ["x-request-id", "x-correlation-id", "x-trace-id"]; const candidates = ["x-request-id", "x-correlation-id", "x-trace-id"];
for (const key of candidates) { for (const key of candidates) {
const value = headers[key]; const value = headers[key];
if (typeof value === "string" && value.trim()) { if (typeof value === "string") {
return value.trim(); const trimmed = value.trim();
if (trimmed && isValidRequestId(trimmed)) {
return trimmed;
}
} }
if (Array.isArray(value) && value[0]) { if (Array.isArray(value) && value[0]) {
return value[0].trim(); const trimmed = value[0].trim();
if (isValidRequestId(trimmed)) {
return trimmed;
}
} }
} }
return generateRequestId(); return generateRequestId();