fix(adapter-utils): read a serialized escape pair as one backslash run

A shell escape pair doubles its backslash with every serialization layer,
so the continuation and opening escape-pair segments now consume the whole
backslash run with the character it escapes. An escaped-space segment
adjacent to a header value inside a serialized command is therefore part
of the value at any depth.

Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE
This commit is contained in:
Michel Tomas 2026-09-05 23:20:04 +02:00
parent f675a08bbd
commit 205c82591b
No known key found for this signature in database
GPG Key ID: 0878846631FFD1E0
2 changed files with 27 additions and 6 deletions

View File

@ -342,6 +342,27 @@ describe("redactCommandText header secrets", () => {
}
});
it("consumes an escaped-space continuation at every serialization depth", () => {
// A shell escape pair doubles its backslash with each serialization
// layer; the continuation reads the whole run as one pair.
const bases = [
'curl -H X-API-Key:"SECRET"\\ TAIL https://example.test',
'curl -H X-API-Key:\\ SECRET https://example.test',
'curl -H "X-API-Key: SECRET"\\ TAIL;echo safe',
];
for (const base of bases) {
let text = base;
for (let depth = 0; depth <= 2; depth += 1) {
if (depth > 0) text = JSON.stringify(text);
const output = redactCommandText(text);
expect(output).not.toContain("SECRET");
expect(output).not.toContain("TAIL");
if (depth > 0) expect(() => JSON.parse(output)).not.toThrow();
expect(redactCommandText(output)).toBe(output);
}
}
});
it("redacts a bare apikey header value", () => {
// Supabase sends the key under an unhyphenated `apikey` header.
expect(redactCommandText("apikey: abc")).toBe(

View File

@ -135,13 +135,13 @@ const COMMAND_SHELL_QUOTED_SEGMENT_PATTERNS = [
String.raw`'[^'\r\n]*'`,
String.raw`\$'(?:\\.|[^'\\\r\n])*'`,
] as const;
const COMMAND_SHELL_ESCAPE_PAIR_PATTERN = String.raw`\\[^\r\n]`;
const COMMAND_SHELL_ESCAPE_PAIR_PATTERN = String.raw`\\+[^\r\n]`;
// An opening escape pair carries the first byte of an unquoted value, as in
// `X-API-Key:\ abc`. It excludes the escaped quote, so a `\"` opener falls to
// the escaped branches. A deeper run such as `\\\"` opens with an escaped
// backslash, which this pattern does accept; the escaped branches precede the
// unquoted one in the alternation and take that value first.
const COMMAND_SHELL_OPENING_ESCAPE_PAIR_PATTERN = String.raw`\\[^"\r\n]`;
// `X-API-Key:\ abc`. The backslash run may be longer inside a serialized
// command, where each layer doubles it. A run followed by a quote is excluded,
// so a `\"` opener at any depth falls to the escaped branches, which precede
// the unquoted one in the alternation.
const COMMAND_SHELL_OPENING_ESCAPE_PAIR_PATTERN = String.raw`\\+[^"\r\n]`;
// The first segment of an unquoted value is a raw token, bounded only by
// whitespace, a quote, a backtick, or a backslash. A raw HTTP diagnostic
// carries an opaque credential the same way, so a `;`, `|`, or `&` inside it