From d273feaa7f552a58f78a9ee80b63d98577c0c16f Mon Sep 17 00:00:00 2001 From: Michel Tomas Date: Sat, 5 Sep 2026 15:09:03 +0200 Subject: [PATCH] fix(adapter-utils): consume escape pairs inside a quoted header value A double-quoted header value stopped at the first backslash, so a credential with an embedded escaped quote such as `"X-API-Key: abc\"def"` kept its tail in the recorded text. The double-quoted branch now consumes escape pairs and requires an unescaped opening quote, which keeps it off a serialized diagnostic where `\"` is the JSON escape. The single-quoted branch takes a backslash literally. Only the unquoted branch still stops at a backslash. Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE --- .../src/command-redaction.test.ts | 29 +++++++++++++++++++ .../adapter-utils/src/command-redaction.ts | 20 ++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/adapter-utils/src/command-redaction.test.ts b/packages/adapter-utils/src/command-redaction.test.ts index 9caa538c50..b68a65ab8c 100644 --- a/packages/adapter-utils/src/command-redaction.test.ts +++ b/packages/adapter-utils/src/command-redaction.test.ts @@ -276,6 +276,35 @@ describe("redactCommandText header secrets", () => { expect(redactDiagnosticText(once)).toBe(once); }); + it("redacts past an escaped quote inside a double-quoted header value", () => { + // The shell escape does not end the argument, so the value runs on past it. + const input = String.raw`curl -H "X-API-Key: abc\"def" https://example.test`; + const output = redactCommandText(input); + expect(output).not.toContain("def"); + expect(output).toBe( + `curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`, + ); + }); + + it("redacts a backslash inside a single-quoted header value", () => { + // A shell single quote has no escapes, so the backslash is part of the value. + const input = String.raw`curl -H 'X-API-Key: abc\def' https://example.test`; + const output = redactCommandText(input); + expect(output).not.toContain("abc"); + expect(output).toBe( + `curl -H 'X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}' https://example.test`, + ); + }); + + it("redacts a double-quoted value that is itself an escaped quoted string", () => { + const input = String.raw`curl -H "Authorization: \"Bearer nested\"" https://example.test`; + const output = redactCommandText(input); + expect(output).not.toContain("nested"); + expect(output).toBe( + `curl -H "Authorization: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`, + ); + }); + it("redacts a header secret inside a diagnostic and keeps a JSON secret field working", () => { const input = 'command failed: curl -H "X-API-Key: abc" -> {"token":"opaque-value"}'; const output = redactDiagnosticText(input); diff --git a/packages/adapter-utils/src/command-redaction.ts b/packages/adapter-utils/src/command-redaction.ts index b761716b32..e401c3b55c 100644 --- a/packages/adapter-utils/src/command-redaction.ts +++ b/packages/adapter-utils/src/command-redaction.ts @@ -45,11 +45,17 @@ const COMMAND_AUTHORIZATION_BEARER_RE = // rule agree byte for byte with the bearer rule above, so // `Authorization: Bearer ` produces the same output as before. // -// Every value branch excludes the backslash. That keeps the rule off an -// escaped-quote opener such as `Authorization: \"Bearer ...\"` in a serialized -// diagnostic, which the caller's own authorization rules already redact. A -// quoted value must open with a non-blank character, so an empty header -// argument such as `-H "X-API-Key: "` stays as it is. +// Each branch treats the backslash the way its quoting context does. A +// double-quoted value consumes escape pairs, so an escaped quote inside the +// argument (`"X-API-Key: abc\"def"`) does not end the value early. Its opening +// quote must itself be unescaped, which keeps the branch off a serialized +// diagnostic such as `\"X-API-Key: ...\"`, where the closing `\"` must survive. +// A single-quoted value takes a backslash literally, because a shell single +// quote has no escapes. Only the unquoted branch stops at a backslash, so an +// escaped-quote opener such as `Authorization: \"Bearer ...\"` is left to the +// caller's own authorization rules. A quoted value must open with a non-blank +// character, so an empty header argument such as `-H "X-API-Key: "` stays as it +// is. // // The schemes come from the IANA HTTP Authentication Scheme Registry, plus // `AWS4-HMAC-SHA256` and `Token`, which are widely used but unregistered. A @@ -89,8 +95,8 @@ const COMMAND_SECRET_HEADER_UNQUOTED_VALUE_PATTERN = "`" + String.raw`]+)`; const COMMAND_SECRET_HEADER_RE = new RegExp( - String.raw`("${COMMAND_SECRET_HEADER_PREFIX_PATTERN})[^\s"\\][^"\\\r\n]*(")` + - String.raw`|('${COMMAND_SECRET_HEADER_PREFIX_PATTERN})[^\s'\\][^'\\\r\n]*(')` + + String.raw`(?