Skip to content

fix(bin): scrub secrets split from stderr to stdout - #6

Merged
Profreshor merged 6 commits into
mainfrom
fm/fm-secrets-split-scrub
Oct 16, 2026
Merged

Profreshor merged 6 commits into
mainfrom
fm/fm-secrets-split-scrub

Conversation

@Profreshor

@Profreshor Profreshor commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Intent

Scrub secrets split from stderr into stdout. The Codex review of fork PR #3 found: when a child writes a credential prefix to stderr and its suffix to stdout, both fragments survive because the check in bin/fm-secrets.py (around lines 487-491) only covers the opposite stdout-prefix/stderr-suffix ordering. For example, splitting 'abcdef' as stderr 'abc' and stdout 'def' exposes every credential byte across the returned streams despite run's scrubbing guarantee. Add the symmetric boundary check and regression coverage. The captain accepted filing this as a follow-up after merging PR #3, and the fix ships as a PR on the Profreshor/firstmate fork. Follow-up decision: the boundary check for both orderings must run on the raw streams before per-stream scrubbing, so overlapping secrets such as TOKEN=foobar plus OTHER_TOKEN=foo leak nothing, with a regression test for that overlap in both orderings using dummy values.

What Changed

  • Redact configured secrets that span stdout and stderr in either stream order before per-stream redaction.
  • Preserve redaction for overlapping secret values by merging raw-stream redaction ranges.
  • Add split-stream regression coverage, including reverse-order and overlapping-secret cases.

Risk Assessment

✅ Low: The raw-coordinate range plan collects all per-stream and both boundary-order matches before mutation, merges them with deterministic scrubber-order labels, and is bounded to the intended secret-redaction behavior.

Testing

The focused behavior suite passed, and live CLI runs spawned real child commands and captured both returned streams for every requested boundary/overlap case; the transcript shows intact redaction markers with no synthetic credential fragments. The change diff was also whitespace-clean.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A secret split stderr-prefix → stdout-suffix is scrubbed ✅ pass live Live transcript: stderr-prefix captures <redacted:TOKEN> on both returned streams.
Opposing raw boundary matches cannot consume one another ✅ pass live Live transcript: both-orders captures intact <redacted:TOKEN> markers on both streams after each raw direction is considered.
Overlapping TOKEN=foobar and OTHER_TOKEN=foo stay redacted in both stream orders ✅ pass live Live transcript: overlap-stdout-prefix and overlap-stderr-prefix contain only redaction markers.
All secret ranges are planned from raw streams before overlap redaction ✅ pass live Live transcript: raw-overlap contains only redaction markers, with no surviving gh suffix.
Evidence: Live fm-secrets stream-scrubbing transcript

Source: Live fm-secrets stream-scrubbing transcript

fm-secrets.sh live stream-scrubbing transcript (all values are synthetic)

[stderr-prefix stdout]
<redacted:TOKEN>
[stderr-prefix stderr]
<redacted:TOKEN>
[overlap-stdout-prefix stdout]
<redacted:TOKEN>
[overlap-stdout-prefix stderr]
<redacted:TOKEN>
[overlap-stderr-prefix stdout]
<redacted:TOKEN>
[overlap-stderr-prefix stderr]
<redacted:TOKEN>
[raw-overlap stdout]
<redacted:TOKEN>
[raw-overlap stderr]
<redacted:TOKEN>
[both-orders stdout]
<redacted:TOKEN>
[both-orders stderr]
<redacted:TOKEN>
Assertions: no synthetic secret, suffix, or cross-stream reconstruction remained in either captured stream.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (3) ✅
  • 🚨 bin/fm-secrets.py:491 - The required “both orderings must run on the raw streams” invariant is still bypassable: if a child writes cab to each stream for TOKEN=abc, the stdout→stderr match replaces stdout’s trailing ab and consumes stderr’s leading c, then break prevents the stderr→stdout check. The returned stderr ab plus stdout c&lt;redacted:TOKEN&gt; reconstructs abc. Handle both raw boundary directions before either mutation can suppress the other, and add this executable regression.

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-secrets.py:507 - Sequentially applying both raw-stream matches can slice into the first replacement. With TOKEN=aaaa and aaa written to each stream, both directions select split 1; stdout becomes aa&lt;redacted:TOKEN&gt; then loses its first three bytes, returning malformed redacted:TOKEN&gt;. Apply edits using original coordinates so both replacement markers remain intact.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-secrets.py:493 - Contradicts the required criterion that “both orderings must run on the raw streams before per-stream scrubbing, so overlapping secrets … leak nothing.” The loop mutates stdout/stderr for the first scrubber before locating the next scrubber's raw boundary. With env-file order TOKEN=abcdef, OTHER_TOKEN=cdefgh, and child output stdout abc, stderr defgh, TOKEN's split-3 edit returns &lt;redacted:TOKEN&gt; and gh; OTHER_TOKEN is then no longer detectable and its gh suffix is disclosed. Compute every scrubber's boundary matches from the original streams, then apply one original-coordinate redaction plan (with defined overlap precedence).

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
A secret split stderr-prefix → stdout-suffix is scrubbed ✅ pass live Live transcript: stderr-prefix captures &lt;redacted:TOKEN&gt; on both returned streams.
Opposing raw boundary matches cannot consume one another ✅ pass live Live transcript: both-orders captures intact &lt;redacted:TOKEN&gt; markers on both streams after each raw direction is considered.
Overlapping TOKEN=foobar and OTHER_TOKEN=foo stay redacted in both stream orders ✅ pass live Live transcript: overlap-stdout-prefix and overlap-stderr-prefix contain only redaction markers.
All secret ranges are planned from raw streams before overlap redaction ✅ pass live Live transcript: raw-overlap contains only redaction markers, with no surviving gh suffix.
  • bin/fm-test-run.sh --jobs 1 tests/fm-secrets.test.sh
  • Manual live invocations of bin/fm-secrets.sh run with separately captured stdout/stderr for symmetric, opposing-direction, and overlapping-secret cases
  • git diff --check 0f8cb488a5d804a4bef6c3be4d1324908f525373 adbc59c7e84cd5655adbc47c9c90d5058c3ec96f
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: addf1d3de5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/fm-secrets.py Outdated
Comment on lines +493 to +495
if stderr.endswith(value[:split]) and stdout.startswith(value[split:]):
stderr = stderr[:-split] + replacement
stdout = stdout[len(value) - split :]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check boundaries before redacting overlapping values

When another scrubber equals the cross-stream prefix—for example, TOKEN=foobar alongside OTHER_TOKEN=foo—the preceding scrub_output calls replace foo before this condition runs. The condition can no longer recognize the split foobar, so stdout returns raw bar, violating the new test's intended neither-fragment guarantee and disclosing part of the selected credential; boundary matching should use the original buffers or occur before per-stream scrubbing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c8b5d8c. Boundary matches for every secret are now collected on the original stdout and stderr (within each stream and across both boundary orders), merged per stream, and redacted in a single pass, so overlapping secrets such as TOKEN=foobar with OTHER_TOKEN=foo no longer leak the suffix. Regression tests cover the foobar/foo overlap in both stream orders, the cab/cab case, and the abcdef/cdefgh case, all with dummy values.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T20:57:13.877642Z addf1d3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Profreshor Profreshor changed the title fix: scrub secrets split from stderr to stdout fix(bin): scrub secrets split from stderr to stdout Oct 16, 2026
@Profreshor
Profreshor merged commit ba356f1 into main Oct 16, 2026
37 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant