fix(adapter-utils): consume a serialized header's adjacent segment and keep a truncated string well formed
A suffix segment adjacent to a serialized quoted header argument belongs to the same shell word, so the serialized branch now takes the shell continuation after its closer. A truncated serialized argument has no closer on its line; its value then stops before a bare quote, one preceded by an even run of backslashes, which can only be the enclosing serializer's delimiter, so that string stays parseable. The closer of an escaped-quoted value must itself be unescaped, so backtracking cannot read an escaped backslash as the closer. Claude-Session: https://claude.ai/code/session_01RYigf3eMFJjey9iKRApPGE
This commit is contained in:
parent
c152eed16a
commit
f675a08bbd
|
|
@ -306,6 +306,42 @@ describe("redactCommandText header secrets", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("consumes a suffix segment adjacent to a serialized quoted header argument", () => {
|
||||
// The suffix is part of the same shell word as the header, so it is part
|
||||
// of the credential at every serialization depth.
|
||||
let text = 'curl -H "X-API-Key: SECRET"TAILMARK;echo safe';
|
||||
for (let depth = 1; depth <= 3; depth += 1) {
|
||||
text = JSON.stringify(text);
|
||||
const output = redactCommandText(text);
|
||||
expect(output).not.toContain("SECRET");
|
||||
expect(output).not.toContain("TAILMARK");
|
||||
expect(output).toContain(";echo safe");
|
||||
expect(() => JSON.parse(output)).not.toThrow();
|
||||
expect(redactCommandText(output)).toBe(output);
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
const cuts = [
|
||||
'curl -H "X-API-Key: SECRET',
|
||||
'curl -H X-API-Key:"SECRET',
|
||||
'curl -H "X-API-Key: SECRET\\',
|
||||
'curl -H "X-API-Key: SECRET\\\\',
|
||||
'curl -H X-API-Key:"SECRET\\',
|
||||
];
|
||||
for (const cut of cuts) {
|
||||
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("redacts a bare apikey header value", () => {
|
||||
// Supabase sends the key under an unhyphenated `apikey` header.
|
||||
expect(redactCommandText("apikey: abc")).toBe(
|
||||
|
|
|
|||
|
|
@ -185,16 +185,27 @@ const commandSecretHeaderEscapedOpener = (run: string) =>
|
|||
String.raw`(?<!\\)(?<${run}>(?:\\\\)*\\)"`;
|
||||
const commandSecretHeaderEscapedBody = (run: string) =>
|
||||
String.raw`(?:(?!(?<!\\)\k<${run}>")[^\s\r\n])(?:(?!(?<!\\)\k<${run}>")[^\r\n])*`;
|
||||
const commandSecretHeaderEscapedCloser = (run: string, close: string) =>
|
||||
String.raw`(?:(?<${close}>\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])*`;
|
||||
const commandSecretHeaderEscapedValue = (run: string, close: string) =>
|
||||
`(?:${commandSecretHeaderEscapedBody(run)}(?<!\\\\)(?<${close}>\\k<${run}>")` +
|
||||
`|${commandSecretHeaderEscapedTruncatedBody(run)}` +
|
||||
String.raw`(?=[\r\n]|$|${COMMAND_SECRET_HEADER_BARE_QUOTE_PATTERN}))`;
|
||||
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}` +
|
||||
String.raw`|(?<ansiPrefix>\$'${COMMAND_SECRET_HEADER_PREFIX_PATTERN})(?:\\.|[^\s'\\])(?:\\.|[^'\\\r\n])*\\?(?:(?<ansiClose>')|(?=[\r\n]|$))${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
`|(?<serPrefix>${commandSecretHeaderEscapedOpener("serRun")}${COMMAND_SECRET_HEADER_PREFIX_PATTERN})` +
|
||||
`${commandSecretHeaderEscapedBody("serRun")}${commandSecretHeaderEscapedCloser("serRun", "serClose")}` +
|
||||
`${commandSecretHeaderEscapedValue("serRun", "serClose")}${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
String.raw`|(?<![A-Za-z0-9_-])(?<evPrefix>\b${COMMAND_SECRET_HEADER_NAME_PATTERN}${COMMAND_SECRET_HEADER_COLON_PATTERN}${COMMAND_SECRET_HEADER_SCHEME_PATTERN}${commandSecretHeaderEscapedOpener("evRun")}${COMMAND_SECRET_HEADER_SCHEME_PATTERN})` +
|
||||
`${commandSecretHeaderEscapedBody("evRun")}${commandSecretHeaderEscapedCloser("evRun", "evClose")}${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
`${commandSecretHeaderEscapedValue("evRun", "evClose")}${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}` +
|
||||
String.raw`|(?<![A-Za-z0-9_-])(?<!(?<!\\)["'])(?<uqPrefix>\b${COMMAND_SECRET_HEADER_PREFIX_PATTERN})${COMMAND_SECRET_HEADER_UNQUOTED_VALUE_PATTERN}${COMMAND_SECRET_HEADER_CONTINUATION_PATTERN}`,
|
||||
"gi",
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue