mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user