agent-sanitizer 2.34.9 → 2.34.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ansi.mjs +25 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sanitizer",
3
- "version": "2.34.9",
3
+ "version": "2.34.10",
4
4
  "description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/ansi.mjs CHANGED
@@ -92,6 +92,13 @@ const ESC = 0x1b;
92
92
  const CSI_C1 = 0x9b;
93
93
  const ST_C1 = 0x9c;
94
94
  const BEL = 0x07;
95
+ // CAN/SUB cancel a control string per ECMA-48 and the xterm parser; LF/CR do
96
+ // not, but bound the body anyway as a fail-closed blast-radius limit (see
97
+ // scanControlString).
98
+ const CAN = 0x18;
99
+ const SUB = 0x1a;
100
+ const LF = 0x0a;
101
+ const CR = 0x0d;
95
102
 
96
103
  // PROBLEM CLASS — a control string whose body the grammar leaves as visible
97
104
  // text. ECMA-48 opens FIVE strings, not one: OSC (`ESC ]` / U+009D), DCS
@@ -209,15 +216,24 @@ export function orphanKindFor(ch, next) {
209
216
  * application command — i.e. attacker-controlled PAYLOAD TEXT in every case.
210
217
  * Consuming the introducer alone would leave that payload in the model's view,
211
218
  * so the whole string is one token. Three ways it can end:
212
- * 1. a real terminator — ST (`ESC\` or the 8-bit C1 ST U+009C) or the legacy
213
- * BEL which is consumed with the body.
219
+ * 1. a real terminator — ST (`ESC\` or the 8-bit C1 ST U+009C), the legacy
220
+ * BEL, or the CAN/SUB (U+0018/U+001A) that ECMA-48 and xterm cancel a
221
+ * string on — which is consumed with the body.
214
222
  * 2. an ABORT: per ECMA-48/xterm a bare ESC (one not forming ST) drops the
215
223
  * terminal out of the string, and a nested C1 string introducer likewise
216
224
  * starts something new. The token ends BEFORE that byte so the scan
217
225
  * re-reads it as its own sequence — without this, an interior ESC deleted
218
- * the rest of the document via case 3.
219
- * 3. end of input, for a genuinely unterminated string: fail closed and drop
220
- * everything from the introducer on, so no body survives.
226
+ * the rest of the document via case 4.
227
+ * 3. a line break (LF/CR) BOUNDS the body, before the break. This is a fail-
228
+ * closed blast-radius limit, NOT terminal behavior: a real terminal ignores
229
+ * an interior LF and keeps collecting to a true terminator. Without the
230
+ * bound one stray `ESC ]` deleted every later line to end of input, so on a
231
+ * consumer that reads the strip as a RECORD (a model, not a display) one
232
+ * introducer blinded the whole tail behind a clean-looking prefix. The
233
+ * break survives; the payload after it on the same line is dropped.
234
+ * 4. end of input, for a genuinely unterminated string with no line break:
235
+ * fail closed and drop everything from the introducer on, so no body
236
+ * survives.
221
237
  *
222
238
  * BEL terminates every arm here, not just OSC. Only xterm's OSC parser accepts
223
239
  * it, so a DCS ending at BEL over-consumes by the width of one body — the
@@ -239,9 +255,11 @@ function scanControlString(text, start) {
239
255
  let i = sevenBit ? start + 2 : start + 1;
240
256
  for (; i < text.length; i++) {
241
257
  const byte = text.charCodeAt(i);
242
- if (byte === BEL || byte === ST_C1) return { end: i + 1, kind };
258
+ if (byte === BEL || byte === ST_C1 || byte === CAN || byte === SUB)
259
+ return { end: i + 1, kind };
243
260
  if (byte === ESC) return { end: text[i + 1] === "\\" ? i + 2 : i, kind };
244
- if (STRING_INTRO_C1.has(byte)) return { end: i, kind };
261
+ if (STRING_INTRO_C1.has(byte) || byte === LF || byte === CR)
262
+ return { end: i, kind };
245
263
  }
246
264
  return { end: text.length, kind };
247
265
  }