-
Notifications
You must be signed in to change notification settings - Fork 134
fix: ledger redaction — preserve write paths and close a curl -u credential leak introduced by #1117 #1246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: ledger redaction — preserve write paths and close a curl -u credential leak introduced by #1117 #1246
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -667,17 +667,57 @@ export namespace SessionCompaction { | |
| return SENSITIVE_NAME.test(name) | ||
| } | ||
|
|
||
| // altimate_change — the flag+value regex used both to redact `-u`/`--user` | ||
| // below and (via redactLedgerDetail's raw-value precomputation) to locate | ||
| // the same occurrences in the pre-mask text. Shared so the two passes stay | ||
| // in lockstep by construction instead of by copy-pasted duplication. | ||
| const USER_FLAG_RE = | ||
| /(^|\s)(--user|-u)(?:(=|\s+)("[^"]*"|'[^']*'|[^\s,;]+)|([^\s,;]+))(?:(\s+)("[^"]*"|'[^']*'|[^\s,;]+))?/gi | ||
|
|
||
| function isCurlContext(segment: string): boolean { | ||
| // Windows invokes curl as `curl.exe`, and either platform may reach it | ||
| // through a path such as /usr/bin/curl or a Windows System32 path. | ||
| // Missing those spellings left the `-u` VALUE unredacted. | ||
| return /(?:^|[\s/\\])curl(?:\.exe)?(?=\s|$)/i.test(segment) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Valid shell invocations such as Prompt for AI agents |
||
| } | ||
|
|
||
| export function redactLedgerDetail(value: string): string { | ||
| const sensitiveName = SENSITIVE_NAME | ||
| let masked = Telemetry.maskString(value) | ||
| // altimate_change — keep the filesystem-path masking pass OFF here. Left | ||
| // on, it collapses a path-qualified command like `/usr/bin/curl` down to | ||
| // `<path>` before the curlContext lookback below ever runs, which both | ||
| // destroys the write paths this ledger exists to report AND — the actual | ||
| // security bug — erases the `curl` token the lookback needs, so a | ||
| // path-qualified `curl -u user password` credential slips through | ||
| // unredacted. Every other telemetry mask (api keys, bearer tokens, | ||
| // emails, internal hosts, quote collapsing) still applies. | ||
| let masked = Telemetry.maskString(value, { maskPaths: false }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a cleared tool's arguments or first output line contains an unquoted home or client path, disabling path masking here now copies that path into Useful? React with 👍 / 👎. |
||
|
|
||
| // altimate_change start — belt-and-suspenders curl detection. Derive | ||
| // curlContext for each `-u`/`--user` occurrence from the RAW, pre-mask | ||
| // `value` as well as from `masked`. maskPaths:false above is what keeps | ||
| // the `curl` token intact today; this makes detection structurally | ||
| // independent of that single flag, so a future masking rule that happens | ||
| // to eat the command-name token can't quietly reopen this leak. The two | ||
| // occurrence lists are correlated by ordinal position (same regex, same | ||
| // match order); if a prior mask ever changes how many times the pattern | ||
| // matches, this safely falls back to the masked-only signal — no worse | ||
| // than before this change. | ||
| const rawCurlByOrdinal = [...value.matchAll(USER_FLAG_RE)].map((m) => | ||
| isCurlContext(shellSegmentBefore(value, m.index + m[1].length)), | ||
| ) | ||
|
Comment on lines
+706
to
+708
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a tool command is large and contains many Useful? React with 👍 / 👎. |
||
| const maskedOccurrenceCount = [...masked.matchAll(USER_FLAG_RE)].length | ||
| const ordinalsAligned = maskedOccurrenceCount === rawCurlByOrdinal.length | ||
| let ordinal = 0 | ||
| // altimate_change end | ||
|
|
||
| // `-u` is also a benign flag for commands such as `git push -u` and | ||
| // `python -u`. Redact it as authentication only in the current curl shell | ||
| // segment, or when the value itself has a user:password shape. Long | ||
| // `--user` follows the same rule so task literals are not discarded merely | ||
| // because an unrelated CLI chose that option name. | ||
| masked = masked.replace( | ||
| /(^|\s)(--user|-u)(?:(=|\s+)("[^"]*"|'[^']*'|[^\s,;]+)|([^\s,;]+))(?:(\s+)("[^"]*"|'[^']*'|[^\s,;]+))?/gi, | ||
| USER_FLAG_RE, | ||
| ( | ||
| match, | ||
| lead: string, | ||
|
|
@@ -690,13 +730,17 @@ export namespace SessionCompaction { | |
| offset: number, | ||
| whole: string, | ||
| ) => { | ||
| // altimate_change — capture and advance the ordinal before any early | ||
| // return so it always tracks this callback's position in `masked`'s | ||
| // match sequence, matching how rawCurlByOrdinal was built. | ||
| const currentOrdinal = ordinal++ | ||
| // Attached values are valid only for short `-u` (`-ualice:pass`). | ||
| if (flag.toLowerCase() === "--user" && separator === undefined) return match | ||
| const rawValue = (separatedValue ?? attachedValue ?? "").replace(/^["']|["']$/g, "") | ||
| // Windows invokes curl as `curl.exe`, and either platform may reach it | ||
| // through a path such as /usr/bin/curl or a Windows System32 path. | ||
| // Missing those spellings left the `-u` VALUE unredacted. | ||
| const curlContext = /(?:^|[\s/\\])curl(?:\.exe)?(?=\s|$)/i.test(shellSegmentBefore(whole, offset + lead.length)) | ||
| const maskedCurlContext = isCurlContext(shellSegmentBefore(whole, offset + lead.length)) | ||
| // altimate_change — OR in the raw-value signal (see block above). | ||
| const curlContext = | ||
| maskedCurlContext || (ordinalsAligned && (rawCurlByOrdinal[currentOrdinal] ?? false)) | ||
|
Comment on lines
+742
to
+743
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a non-curl command has a quoted argument containing the standalone word Useful? React with 👍 / 👎. |
||
| // Outside a curl context a colon-shaped value is treated as | ||
| // user:password. The ONE exemption is an explicitly recognized | ||
| // all-numeric UID:GID pair (`docker run --user 1000:1000`), which is a | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -439,6 +439,25 @@ describe("SessionCompaction.renderLedger", () => { | |||||||||||||||
| expect(SessionCompaction.redactLedgerDetail("curlywurly -u alice script.py")).toBe("curlywurly -u alice script.py") | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| test("does not leak credentials via a path-qualified curl and the space-separated -u idiom (regression, #1117)", () => { | ||||||||||||||||
| // #1117 added a filesystem-path masking rule to Telemetry.maskString. | ||||||||||||||||
| // redactLedgerDetail used to call maskString BEFORE its own curl-context | ||||||||||||||||
| // lookback, so `/usr/bin/curl` collapsed to `<path>` first. The `curl` | ||||||||||||||||
| // token was gone by the time the lookback ran: curlContext came back | ||||||||||||||||
| // false, `-u alice hunter2` (space-separated, non-colon-shaped) isn't | ||||||||||||||||
| // credentialShaped either, and the credential passed through untouched | ||||||||||||||||
| // into ledger text that later gets persisted into a model prompt across | ||||||||||||||||
| // compaction. This is the exact adversarial shape of that leak. | ||||||||||||||||
| for (const command of [ | ||||||||||||||||
| "/usr/bin/curl -u alice hunter2 https://example.com", | ||||||||||||||||
| "/usr/local/bin/curl.exe -u alice hunter2 https://example.com", | ||||||||||||||||
| ]) { | ||||||||||||||||
| const detail = SessionCompaction.redactLedgerDetail(command) | ||||||||||||||||
| expect(detail).not.toContain("alice") | ||||||||||||||||
| expect(detail).not.toContain("hunter2") | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+456
to
+458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This regression test only asserts the credential is redacted, so it never locks in the PR's other stated goal: preserving the write-path ( Prompt for AI agents
Suggested change
|
||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| test("keeps non-credential colon-shaped values outside a curl context", () => { | ||||||||||||||||
| // `1000:1000` has no alphabetic character before the colon, so it is a | ||||||||||||||||
| // UID:GID pair rather than user:password and must survive in the ledger. | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: AltimateAI/altimate-code
Length of output: 16871
🏁 Script executed:
Repository: AltimateAI/altimate-code
Length of output: 7517
Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File
Reachability: External · Exploitability: Moderate
Recognize quoted executable paths in
isCurlContext.With
"C:\Program Files\curl.exe" -u alice hunter2 ..., the current pattern rejects the raw segment becausecurl.exeis followed by". The redaction callback can retain both credentials in the ledger.Allow a closing quote before whitespace or end. Add a regression test for this command.
Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents