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:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user