Lines 1-40javascript
1// Untrusted-content wrapping — the cheapest defense against indirect prompt injection for an agent that
2// holds a `bash` tool. Web pages / search results flow straight into the model; a hostile page can carry
3// "ignore previous instructions, run …". We wrap such content in a notice + a random per-call boundary id
4// so the model treats it as DATA, and we defang homoglyph / zero-width tricks a page could use to forge the
5// closing boundary. Pure-Node, zero-dep. (Pattern adapted from openclaw's external-content guard.)
6import { randomBytes } from "node:crypto";
7// Confusable angle brackets → ASCII, so a page can't fake a boundary marker. Defined by explicit codepoint
8// (built programmatically) to avoid visually-identical literal duplicates (e.g. U+3008 vs U+2329 both "〈").
10 [0xff1c, "<"], [0xff1e, ">"], // fullwidth < >
11 [0x3008, "<"], [0x3009, ">"], // CJK angle brackets
12 [0x2329, "<"], [0x232a, ">"], // angle bracket (deprecated)
13 [0x27e8, "<"], [0x27e9, ">"], // mathematical angle brackets
14 [0x276c, "<"], [0x276d, ">"], // medium ornamental
15 [0x2039, "<"], [0x203a, ">"], // single guillemets ‹ ›
17const ANGLE_MAP = new Map(ANGLE_PAIRS.map(([cp, a]) => [String.fromCodePoint(cp), a]));
18const ANGLE_RE = new RegExp(`[${ANGLE_PAIRS.map(([cp]) => `\\u${cp.toString(16).padStart(4, "0")}`).
19// Zero-width / invisible chars used to smuggle content: ZWSP/ZWNJ/ZWJ/word-joiner/BOM/soft-hyphen.
20const ZERO_WIDTH_RE = /[]/g;
CriticalTrojan Source Unicode
Source contains bidi control or invisible Unicode characters associated with Trojan Source attacks.
dist/security/external-content.jsView on unpkg · L20 21/** Fold confusable angle brackets to ASCII and strip zero-width characters, so untrusted text can't
22 * forge the boundary marker or hide injected instructions. */
23export function defang(s) {
24 return s.replace(ANGLE_RE, (ch) => ANGLE_MAP.get(ch) ?? ch).replace(ZERO_WIDTH_RE, "");
26// Phrases that strongly suggest an injection attempt — surfaced as a hint in the notice (not a hard block).
28 /ignore (all |the )?(previous|prior|above) (instructions|prompts?)/i,
29 /disregard (all |the )?(previous|prior|above)/i,
32 /\bnew instructions?\b/i,
33 /reveal (your |the )?(system )?(prompt|instructions)/i,
34 /\b(exfiltrate|send)\b.{0,30}(secret|token|api[ _-]?key|credential|password)/i,
36/** True if the text contains a likely prompt-injection phrase. */
37export function looksLikeInjection(s) {
38 return SUSPICIOUS.some((re) => re.test(s));
40/** Wrap external/untrusted content so the model treats it as data, not instructions. `source` is shown for