fix(adapter-utils): let the header scanner own the bearer header
The bearer-only rule stopped its value at a quote but not at a backslash, so inside a serialized command it consumed the backslash of the escaped closing quote and the stored JSON string no longer parsed (upstream #11037). The header scanner already covers Authorization with any scheme: it reads the serialization layer off the delimiter, keeps the scheme word, and redacts the whole header word, so a value carrying a backslash or an escaped quote is redacted whole instead of cut. The bearer rule is removed; its byte-for-byte pin stays, and four tests pin serialized commands at depths 1 to 3, a JSON object carrying the command, a backslash inside the value, and a raw escaped quote. Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE
This commit is contained in:
parent
6610e72825
commit
1aae093b51
|
|
@ -95,6 +95,55 @@ second-line\" status=401`;
|
|||
});
|
||||
});
|
||||
|
||||
describe("redactCommandText bearer headers", () => {
|
||||
const command = 'curl -H "Authorization: Bearer abc" https://example.test';
|
||||
const expected = `curl -H "Authorization: Bearer ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`;
|
||||
|
||||
it("keeps a serialized command parseable after redacting its bearer header", () => {
|
||||
// The former bearer-only rule consumed the backslash of the escaped
|
||||
// closing quote, and the enclosing JSON string stopped parsing.
|
||||
let serialized = command;
|
||||
for (let depth = 1; depth <= 3; depth += 1) {
|
||||
serialized = JSON.stringify(serialized);
|
||||
const output = redactCommandText(serialized);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
let decoded: string = output;
|
||||
for (let layer = 0; layer < depth; layer += 1) {
|
||||
decoded = JSON.parse(decoded);
|
||||
}
|
||||
expect(decoded).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a serialized JSON object carrying a bearer command parseable", () => {
|
||||
const input = JSON.stringify({ command, cwd: "/tmp" });
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(JSON.parse(output)).toEqual({ command: expected, cwd: "/tmp" });
|
||||
});
|
||||
|
||||
it("redacts a bearer value that carries a backslash whole", () => {
|
||||
const input = String.raw`curl -H "Authorization: Bearer abc\tail" https://example.test`;
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(output).not.toContain("tail");
|
||||
expect(output).toBe(expected);
|
||||
});
|
||||
|
||||
it("redacts a raw bearer value that carries an escaped quote whole", () => {
|
||||
// `abc\"def` is one shell word. Read as a serialized opener with no closer,
|
||||
// the escaped quote runs the value to the end of the line, so the union
|
||||
// takes the following argument too. Over-redaction, never a leak.
|
||||
const input = String.raw`curl -H Authorization: Bearer abc\"def https://example.test`;
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(output).not.toContain("def");
|
||||
expect(output).toBe(`curl -H Authorization: Bearer ${REDACTED_COMMAND_TEXT_VALUE}`);
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactCommandText header secrets", () => {
|
||||
it("redacts a double-quoted X-API-Key header value", () => {
|
||||
const input = 'curl -H "X-API-Key: abc" https://example.test/api/agents/me';
|
||||
|
|
@ -135,8 +184,8 @@ describe("redactCommandText header secrets", () => {
|
|||
});
|
||||
|
||||
it("keeps the bearer header output byte for byte identical", () => {
|
||||
// The bearer rule already redacted this shape. The header rule keeps the
|
||||
// scheme, so the output must not change.
|
||||
// The former bearer-only rule redacted this shape. The header rule keeps
|
||||
// the scheme, so the output must not change.
|
||||
const input = 'curl -H "Authorization: Bearer abc" https://example.test';
|
||||
expect(redactCommandText(input)).toBe(
|
||||
`curl -H "Authorization: Bearer ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@ const COMMAND_ENV_SECRET_ASSIGNMENT_RE = new RegExp(
|
|||
String.raw`]+))`,
|
||||
"gi",
|
||||
);
|
||||
const COMMAND_AUTHORIZATION_BEARER_RE =
|
||||
/(\bAuthorization\s*:\s*Bearer\s+)[^\s"'`]+/gi;
|
||||
// `Authorization: Bearer <value>` is owned by the header rule below. It used to
|
||||
// have a rule of its own whose value class stopped at a quote but not at a
|
||||
// backslash, so inside a serialized command it consumed the backslash of the
|
||||
// escaped closing quote and the stored JSON string no longer parsed. The header
|
||||
// rule reads the serialization layer off the delimiter instead, keeps the
|
||||
// scheme word, and redacts the whole header word, so a value that itself
|
||||
// carries a backslash or an escaped quote is redacted whole rather than cut.
|
||||
// A secret-bearing header names a credential in its own header name. The public
|
||||
// Paperclip API documents `X-API-Key`, and a run log can also carry `Api-Key`,
|
||||
// `X-Auth-Token`, `X-Paperclip-Api-Key`, or a bare `Authorization`. This rule
|
||||
|
|
@ -71,8 +76,8 @@ const COMMAND_SECRET_HEADER_CANDIDATE_RE = new RegExp(
|
|||
// A scheme may also follow the value's own escaped-quote delimiter, as in
|
||||
// `Authorization:\"Digest username=...\"`. An optional auth scheme stays in the
|
||||
// output: it is not a secret, and it tells a reader which credential form the
|
||||
// command used. This also makes the rule agree with the bearer rule above for a
|
||||
// well-formed bearer header.
|
||||
// command used, and it keeps the output of a well-formed bearer header
|
||||
// byte-identical to what the former bearer-only rule produced.
|
||||
const COMMAND_SECRET_HEADER_SCHEME_AT_RE = new RegExp(
|
||||
String.raw`(?:${COMMAND_AUTH_SCHEMES.join("|")})[ \t]+`,
|
||||
"iy",
|
||||
|
|
@ -842,10 +847,7 @@ export function redactCommandText(
|
|||
redactedValue = REDACTED_COMMAND_TEXT_VALUE,
|
||||
): string {
|
||||
if (!maybeContainsSecretText(command)) return command;
|
||||
return redactCommandSecretHeaders(
|
||||
command.replace(COMMAND_AUTHORIZATION_BEARER_RE, `$1${redactedValue}`),
|
||||
redactedValue,
|
||||
)
|
||||
return redactCommandSecretHeaders(command, redactedValue)
|
||||
.replace(COMMAND_CLI_SECRET_OPTION_RE, `$1${redactedValue}$3`)
|
||||
.replace(
|
||||
COMMAND_ENV_SECRET_ASSIGNMENT_RE,
|
||||
|
|
|
|||
Loading…
Reference in New Issue