Security: add per-wrapper IDs to untrusted-content markers (#19009)

Fixes #10927

Adds unique per-wrapper IDs to external-content boundary markers to
prevent spoofing attacks where malicious content could inject fake
marker boundaries.

- Generate random 16-char hex ID per wrap operation
- Start/end markers share the same ID for pairing
- Sanitizer strips markers with or without IDs (handles legacy + spoofed)
- Added test for attacker-injected markers with fake IDs

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
C.J. Winslow
2026-02-20 22:16:02 -08:00
committed by GitHub
parent 45fff13b1d
commit 58f7b7638a
4 changed files with 82 additions and 37 deletions

View File

@@ -1,3 +1,5 @@
import { randomBytes } from "node:crypto";
/**
* Security utilities for handling untrusted external content.
*
@@ -43,9 +45,23 @@ export function detectSuspiciousPatterns(content: string): string[] {
/**
* Unique boundary markers for external content.
* Using XML-style tags that are unlikely to appear in legitimate content.
* Each wrapper gets a unique random ID to prevent spoofing attacks where
* malicious content injects fake boundary markers.
*/
const EXTERNAL_CONTENT_START = "<<<EXTERNAL_UNTRUSTED_CONTENT>>>";
const EXTERNAL_CONTENT_END = "<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>";
const EXTERNAL_CONTENT_START_NAME = "EXTERNAL_UNTRUSTED_CONTENT";
const EXTERNAL_CONTENT_END_NAME = "END_EXTERNAL_UNTRUSTED_CONTENT";
function createExternalContentMarkerId(): string {
return randomBytes(8).toString("hex");
}
function createExternalContentStartMarker(id: string): string {
return `<<<${EXTERNAL_CONTENT_START_NAME} id="${id}">>>`;
}
function createExternalContentEndMarker(id: string): string {
return `<<<${EXTERNAL_CONTENT_END_NAME} id="${id}">>>`;
}
/**
* Security warning prepended to external content.
@@ -130,9 +146,10 @@ function replaceMarkers(content: string): string {
return content;
}
const replacements: Array<{ start: number; end: number; value: string }> = [];
// Match markers with or without id attribute (handles both legacy and spoofed markers)
const patterns: Array<{ regex: RegExp; value: string }> = [
{ regex: /<<<EXTERNAL_UNTRUSTED_CONTENT>>>/gi, value: "[[MARKER_SANITIZED]]" },
{ regex: /<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>/gi, value: "[[END_MARKER_SANITIZED]]" },
{ regex: /<<<EXTERNAL_UNTRUSTED_CONTENT(?:\s+id="[^"]{1,128}")?\s*>>>/gi, value: "[[MARKER_SANITIZED]]" },
{ regex: /<<<END_EXTERNAL_UNTRUSTED_CONTENT(?:\s+id="[^"]{1,128}")?\s*>>>/gi, value: "[[END_MARKER_SANITIZED]]" },
];
for (const pattern of patterns) {
@@ -209,14 +226,15 @@ export function wrapExternalContent(content: string, options: WrapExternalConten
const metadata = metadataLines.join("\n");
const warningBlock = includeWarning ? `${EXTERNAL_CONTENT_WARNING}\n\n` : "";
const markerId = createExternalContentMarkerId();
return [
warningBlock,
EXTERNAL_CONTENT_START,
createExternalContentStartMarker(markerId),
metadata,
"---",
sanitized,
EXTERNAL_CONTENT_END,
createExternalContentEndMarker(markerId),
].join("\n");
}