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

@@ -269,7 +269,7 @@ describe("web_search external content wrapping", () => {
results?: Array<{ description?: string }>;
};
expect(details.results?.[0]?.description).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(details.results?.[0]?.description).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(details.results?.[0]?.description).toContain("Ignore previous instructions");
expect(details.externalContent).toMatchObject({
untrusted: true,
@@ -332,7 +332,7 @@ describe("web_search external content wrapping", () => {
const result = await executePerplexitySearchForWrapping("test");
const details = result?.details as { content?: string };
expect(details.content).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(details.content).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(details.content).toContain("Ignore previous instructions");
});

View File

@@ -168,7 +168,7 @@ describe("web_fetch extraction fallbacks", () => {
externalContent?: { untrusted?: boolean; source?: string; wrapped?: boolean };
};
expect(details.text).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(details.text).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(details.text).toContain("Ignore previous instructions");
expect(details.externalContent).toMatchObject({
untrusted: true,
@@ -332,7 +332,7 @@ describe("web_fetch extraction fallbacks", () => {
maxChars: 200_000,
});
const details = result?.details as { text?: string; length?: number; truncated?: boolean };
expect(details.text).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(details.text).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(details.text).toContain("Source: Web Fetch");
expect(details.length).toBeLessThanOrEqual(10_000);
expect(details.truncated).toBe(true);
@@ -358,7 +358,7 @@ describe("web_fetch extraction fallbacks", () => {
});
expect(message).toContain("Web fetch failed (404):");
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(message).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(message).toContain("SECURITY NOTICE");
expect(message).toContain("Not Found");
expect(message).not.toContain("<html");
@@ -380,7 +380,7 @@ describe("web_fetch extraction fallbacks", () => {
});
expect(message).toContain("Web fetch failed (500):");
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(message).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(message).toContain("Oops");
});
@@ -407,7 +407,7 @@ describe("web_fetch extraction fallbacks", () => {
});
expect(message).toContain("Firecrawl fetch failed (403):");
expect(message).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT>>>");
expect(message).toMatch(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]{16}">>>/);
expect(message).toContain("blocked");
});
});