fix(adapter-utils): consume an even escape run whole and a truncated trailing segment
The even-run escape pair now requires a character that is neither a quote nor a backslash after the run, so a run of any even length is consumed by the odd alternative as pairs plus an escaped backslash and the following quote opens a segment of the same word. The continuation may end with one unterminated quoted segment that runs to the end of the line, so a log cut inside the last segment of a header word still redacts it. Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE
This commit is contained in:
parent
bf30a05e64
commit
808854d9af
|
|
@ -296,14 +296,37 @@ describe("redactCommandText header secrets", () => {
|
|||
});
|
||||
|
||||
it("reads an even backslash run before a quote as a bare quote", () => {
|
||||
// `\\"` is an escaped backslash followed by a real quote, not an escaped
|
||||
// `\\\\"` is an escaped backslash followed by a real quote, not an escaped
|
||||
// quote, so the escaped branches decline it and the value still redacts.
|
||||
const input = String.raw`foo\\"X-API-Key: abc" bar`;
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(output).toBe(
|
||||
String.raw`foo\\"X-API-Key: ` + REDACTED_COMMAND_TEXT_VALUE + '" bar',
|
||||
);
|
||||
expect(output).toBe(String.raw`foo\\"X-API-Key: ` + REDACTED_COMMAND_TEXT_VALUE);
|
||||
});
|
||||
|
||||
it("consumes an even backslash run of any length before a segment quote", () => {
|
||||
for (const run of ["\\\\", "\\\\\\\\", "\\\\\\\\\\\\"]) {
|
||||
const input = `curl -H X-API-Key:SECRET${run}"TAILMARK"MORE ;echo safe`;
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("SECRET");
|
||||
expect(output).not.toContain("TAILMARK");
|
||||
expect(output).not.toContain("MORE");
|
||||
expect(output).toBe(`curl -H X-API-Key:${REDACTED_COMMAND_TEXT_VALUE} ;echo safe`);
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
}
|
||||
});
|
||||
|
||||
it("redacts a truncated quoted tail after a closed escaped value", () => {
|
||||
for (const tail of ["'TAILMARK", '"TAILMARK', "$'TAILMARK"]) {
|
||||
let text = `curl -H X-API-Key:\\"SECRET\\"${tail}`;
|
||||
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("TAILMARK");
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("consumes a suffix segment adjacent to a serialized quoted header argument", () => {
|
||||
|
|
@ -540,13 +563,14 @@ describe("redactCommandText header secrets", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps an unterminated quote out of the value", () => {
|
||||
// A lone quote does not open a segment, so the word ends before it.
|
||||
const input = String.raw`X-API-Key: abc"tail`;
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("abc");
|
||||
expect(output).toBe(
|
||||
`X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}"tail`,
|
||||
it("redacts an unterminated quoted tail as a truncated segment", () => {
|
||||
// A quote with no closer on the line is read as a segment of the same
|
||||
// word cut by the log, so its text is redacted rather than kept.
|
||||
expect(redactCommandText('X-API-Key: abc"tail')).toBe(
|
||||
`X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}`,
|
||||
);
|
||||
expect(redactCommandText('curl -H "X-API-Key: abc" "other')).toBe(
|
||||
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" "other`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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]|(?:\\\\)*\\[^\r\n])`;
|
||||
const COMMAND_SHELL_ESCAPE_PAIR_PATTERN = String.raw`(?:(?:\\\\)+[^"\\\r\n]|(?:\\\\)*\\[^\r\n])`;
|
||||
// An opening escape pair carries the first byte of an unquoted value, as in
|
||||
// `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]|(?:\\\\)*\\[^"\r\n])`;
|
||||
const COMMAND_SHELL_OPENING_ESCAPE_PAIR_PATTERN = String.raw`(?:(?:\\\\)+[^"\\\r\n]|(?:\\\\)*\\[^"\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
|
||||
|
|
@ -158,7 +158,17 @@ const COMMAND_SHELL_SEGMENT_PATTERN = `(?:${[
|
|||
COMMAND_SHELL_ESCAPE_PAIR_PATTERN,
|
||||
COMMAND_SHELL_PLAIN_SEGMENT_PATTERN,
|
||||
].join("|")})`;
|
||||
const COMMAND_SECRET_HEADER_CONTINUATION_PATTERN = `${COMMAND_SHELL_SEGMENT_PATTERN}*`;
|
||||
// A run log can cut a line inside the last segment of the word. A quoted
|
||||
// segment with no closer on the line is still part of the value, so the
|
||||
// continuation may end with one unterminated quoted segment that runs to the
|
||||
// end of the line.
|
||||
const COMMAND_SHELL_TRUNCATED_SEGMENT_PATTERN =
|
||||
String.raw`(?:"(?:\.|[^"\
|
||||
])*|\$'(?:\.|[^'\
|
||||
])*|'[^'
|
||||
]*)(?=[
|
||||
]|$)`;
|
||||
const COMMAND_SECRET_HEADER_CONTINUATION_PATTERN = `${COMMAND_SHELL_SEGMENT_PATTERN}*(?:${COMMAND_SHELL_TRUNCATED_SEGMENT_PATTERN})?`;
|
||||
// A value quoted after the colon keeps its own delimiters around the
|
||||
// placeholder, so a second pass reads the same shape and leaves it alone. The
|
||||
// opener and the closer are captured; the body is not.
|
||||
|
|
|
|||
Loading…
Reference in New Issue