fix(adapter-utils): read an even backslash run before a quote as a bare quote
An escape-pair segment is either an even backslash run followed by a character other than a quote, the form a serialized escaped space takes, or an odd run followed by any character, a true shell escape. An even run before a quote is an escaped backslash and the quote opens a further segment of the same word, which the value consumes. A truncated escaped-quoted value runs to the end of its line again: a bare quote there may be a further segment of the word, so it is redacted rather than kept as an enclosing delimiter. Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE
This commit is contained in:
parent
205c82591b
commit
bf30a05e64
|
|
@ -321,10 +321,11 @@ describe("redactCommandText header secrets", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps a serializer's closing delimiter when the argument is truncated", () => {
|
||||
// A run log can cut a serialized command inside the header argument. The
|
||||
// truncated value stops before the enclosing string's own quote, even
|
||||
// when the cut lands after a backslash, so the string stays well formed.
|
||||
it("redacts a truncated serialized argument to the end of its line", () => {
|
||||
// A run log can cut a serialized command inside the header argument. With
|
||||
// no closer on the line, the value runs to the end of the line: a bare
|
||||
// quote there may be a further segment of the same shell word, so the rule
|
||||
// redacts it rather than keeping it as the enclosing string's delimiter.
|
||||
const cuts = [
|
||||
'curl -H "X-API-Key: SECRET',
|
||||
'curl -H X-API-Key:"SECRET',
|
||||
|
|
@ -336,12 +337,32 @@ describe("redactCommandText header secrets", () => {
|
|||
for (const text of [JSON.stringify(cut), JSON.stringify(JSON.stringify(cut))]) {
|
||||
const output = redactCommandText(text);
|
||||
expect(output).not.toContain("SECRET");
|
||||
expect(() => JSON.parse(output)).not.toThrow();
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an even backslash run before a quote out of the escape pair", () => {
|
||||
// Two backslashes are an escaped backslash; the quote after them opens a
|
||||
// further segment of the same word, which is consumed with the value.
|
||||
const input = 'curl -H X-API-Key:SECRET\\\\"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 an escaped-quoted value", () => {
|
||||
const input = 'curl -H X-API-Key:\\"SECRET"TAILMARK';
|
||||
const output = redactCommandText(input);
|
||||
expect(output).not.toContain("SECRET");
|
||||
expect(output).not.toContain("TAILMARK");
|
||||
expect(output).toBe(`curl -H X-API-Key:\\"${REDACTED_COMMAND_TEXT_VALUE}`);
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
});
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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]|(?:\\\\)*\\[^\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]`;
|
||||
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
|
||||
|
|
@ -185,19 +185,14 @@ const commandSecretHeaderEscapedOpener = (run: string) =>
|
|||
String.raw`(?<!\\)(?<${run}>(?:\\\\)*\\)"`;
|
||||
const commandSecretHeaderEscapedBody = (run: string) =>
|
||||
String.raw`(?:(?!(?<!\\)\k<${run}>")[^\s\r\n])(?:(?!(?<!\\)\k<${run}>")[^\r\n])*`;
|
||||
// A truncated argument has no closer on its line. Its body then also stops
|
||||
// before a bare quote, one preceded by an even run of backslashes, which can
|
||||
// only be an enclosing serializer's delimiter; that delimiter survives and the
|
||||
// enclosing string stays well formed. The closer itself is unescaped, so
|
||||
// backtracking never reads an escaped backslash as the closer.
|
||||
const COMMAND_SECRET_HEADER_BARE_QUOTE_PATTERN = String.raw`(?<=(?<!\\)(?:\\\\)*)"`;
|
||||
const commandSecretHeaderEscapedTruncatedBody = (run: string) =>
|
||||
String.raw`(?:(?!(?<!\\)\k<${run}>")(?!${COMMAND_SECRET_HEADER_BARE_QUOTE_PATTERN})[^\s\r\n])` +
|
||||
String.raw`(?:(?!(?<!\\)\k<${run}>")(?!${COMMAND_SECRET_HEADER_BARE_QUOTE_PATTERN})[^\r\n])*`;
|
||||
// The closer itself is unescaped, so backtracking never reads an escaped
|
||||
// backslash as the closer. A truncated argument has no closer on its line and
|
||||
// the value then runs to the end of the line: a bare quote there may be a
|
||||
// further segment of the same shell word, so it is redacted rather than kept.
|
||||
const commandSecretHeaderEscapedValue = (run: string, close: string) =>
|
||||
`(?:${commandSecretHeaderEscapedBody(run)}(?<!\\\\)(?<${close}>\\k<${run}>")` +
|
||||
`|${commandSecretHeaderEscapedTruncatedBody(run)}` +
|
||||
String.raw`(?=[\r\n]|$|${COMMAND_SECRET_HEADER_BARE_QUOTE_PATTERN}))`;
|
||||
`|${commandSecretHeaderEscapedBody(run)}` +
|
||||
String.raw`(?=[\r\n]|$))`;
|
||||
const COMMAND_SECRET_HEADER_RE = new RegExp(
|
||||
String.raw`(?<!\\)(?<dqPrefix>"${COMMAND_SECRET_HEADER_PREFIX_PATTERN})(?:\\.|[^\s"\\])(?:\\\r?\n|\\.|[^"\\\r\n])*\\?(?:(?<dqClose>")|(?=[\r\n]|$))${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
String.raw`|(?<sqPrefix>'${COMMAND_SECRET_HEADER_PREFIX_PATTERN})[^\s'][^'\r\n]*(?:(?<sqClose>')|(?=[\r\n]|$))${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
|
|
|
|||
Loading…
Reference in New Issue