Skip to content

Prevent safe-output artifacts from capturing base64 CI trigger token - #50636

Open
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/safe-output-artifacts-fix
Open

Prevent safe-output artifacts from capturing base64 CI trigger token#50636
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/safe-output-artifacts-fix

Conversation

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Safe-output jobs upload raw process-safe-outputs.stdout/stderr.log. On create/push safe-output paths using GH_AW_CI_TRIGGER_TOKEN, the credential-bearing git config command line was echoed to stdout by @actions/exec (default silent: false), teed into those artifacts, and reversible via base64 -d.

Changes (actions/setup/js/git_auth_helpers.cjs)

  • Silence credential-bearing exec calls — added { silent: true } to every exec.exec("git", ["config", ...]) that writes the Authorization: basic <base64> extraheader, in both overridePersistedExtraheader (write path) and restorePersistedExtraheader (restore path, whose previousValues may also carry checkout credentials). This is the primary fix: the header no longer reaches stdout or the uploaded files.
  • Defense-in-depth masking — register the derived base64 value with core.setSecret(tokenBase64). Note this alone is insufficient, since the artifacts are written before runner-side masking; the silent flag is what prevents capture.
core.setSecret(tokenBase64);
// ...
await exec.exec("git", ["config", "--local", "--replace-all", key, authHeader], { cwd, silent: true });

Tests (git_auth_helpers.test.cjs)

  • Updated existing assertions to expect the options object; added a focused test verifying setSecret is called with the base64 value and that all exec.exec calls are silenced.

Notes

  • These .cjs files are copied to the runner at execution time from the actions/setup composite action (not inlined into .lock.yml); make recompile produces no lock diff, so no consumer regeneration is required in this repo. External consumers pick up the fix once released.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Hey 👋 — thanks for getting started on the critical security fix for CI trigger token exposure in safe-output artifacts! This is well-scoped and addresses a real vulnerability.

Since this is still a WIP with no changes yet, here are the next steps to move it forward:

  • Add test coverage — once the fix is in place, include unit tests for the redaction or silent-mode logic to ensure credential-bearing commands are properly excluded from artifact files.
  • Update description as you progress — you've documented the issue well; keep the PR body updated as you implement each remediation step so reviewers can track the work.
  • Reference the issue consistently — the PR correctly links to Safe-output artifacts can expose base64 CI trigger tokens #50616; ensure commits reference it too.

The PR is on the right track. Once changes are committed and tests are in place, this should be ready for review. Keep pushing! 🚀

Generated by ✅ Contribution Check · auto · 46.3 AIC · ⌖ 4.76 AIC · ⊞ 8.8K ·

… header

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix exposure of CI trigger tokens in safe outputs Prevent safe-output artifacts from capturing base64 CI trigger token Aug 5, 2026
Copilot AI requested a review from pelikhan August 5, 2026 17:29
@pelikhan
pelikhan marked this pull request as ready for review August 5, 2026 17:35
Copilot AI balanced review requested due to automatic review settings August 5, 2026 17:35
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #50636 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — approving with one minor test coverage suggestion.

📋 Key Themes & Highlights

Key Themes

  • Root cause properly addressed: silent: true prevents the credential-bearing command line from reaching stdout and uploaded artifacts — not just masking symptoms
  • Defense-in-depth: core.setSecret(tokenBase64) registers the value for runner-side redaction as a secondary guard
  • Comprehensive branch coverage: both cwd and non-cwd branches in both overridePersistedExtraheader and restorePersistedExtraheader are silenced

Positive Highlights

  • ✅ Clear PR description explaining primary vs. secondary fix and why setSecret alone is insufficient
  • ✅ New focused test verifies both setSecret is called with the correct base64 value and that all exec calls are silenced
  • ✅ Existing test assertions updated to enforce silent: true contract going forward
  • ✅ No consumer regeneration required — clean deploy path via runtime file copy

Minor Gap (inline comment)

The "all exec calls silenced" loop in the new test only runs inside overridePersistedExtraheader scope. A parallel assertion for restorePersistedExtraheader would close the regression window on the restore path. See inline comment for a suggested test.

> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31030723085)* · sonnet46 · 28.5 AIC · ⌖ 8.04 AIC · ⊞ 7.1K > Comment /matt to run again

Comments that could not be inline-anchored

actions/setup/js/git_auth_helpers.test.cjs:332

[/tdd] The "never echo via exec" loop only covers overridePersistedExtraheader call sites — the restorePersistedExtraheader exec calls are not included in this sweep.

<details>
<summary>💡 Suggested addition</summary>

Add a parallel assertion in the restorePersistedExtraheader describe block:

it(&#39;should silence all exec calls during restore&#39;, async () =&gt; {
  const prevHeader = `Authorization: basic ${Buffer.from(&#39;x-access-token:ghp_prev&#39;).toString(&#39;base64&#39;)}`;
  await restore…

</details>

@github-actions github-actions Bot mentioned this pull request Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Prevents credential-bearing Git commands from leaking into safe-output artifacts.

Changes:

  • Silences Git configuration commands containing authorization headers.
  • Adds masking registration and related tests.
Show a summary per file
File Description
actions/setup/js/git_auth_helpers.cjs Silences credential-bearing Git commands and adds masking.
actions/setup/js/git_auth_helpers.test.cjs Updates assertions for silent execution and masking.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
// masking redacts it if it ever leaks. This is defense-in-depth; the silent
// exec calls below prevent the credential-bearing command line from being
// written to stdout (and thus to uploaded artifacts) in the first place.
core.setSecret(tokenBase64);
await overridePersistedExtraheader(SERVER_URL, token);

// The base64 header is registered for runner-side masking.
expect(mockCore.setSecret).toHaveBeenCalledWith(tokenBase64);

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fix correctly addresses the credential-leakage path. Both the primary mitigation (silent: true on all credential-bearing exec.exec calls) and the defense-in-depth layer (core.setSecret) are in place and well-tested. No blocking issues.> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 11.2 AIC · ⌖ 7.51 AIC · ⊞ 5.4K

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: sound, targeted fix — one non-blocking diagnosability regression noted

The core fix is correct and minimal: silencing the credential-bearing git config exec calls addresses the actual leak vector (the echoed command line in uploaded stdout/stderr artifacts), and core.setSecret adds reasonable defense-in-depth. No other unsilenced code paths in this file carry the same credential (the read paths in getExtraheaderValues, unsetExtraheaderAllScopes, and findIncludedExtraheaderConfigFiles were already silent: true before this PR). Tests are updated consistently.

💡 Themes / notes
  • One flagged issue: silent: true suppresses stderr as well as stdout, so genuine git config failures (lock contention, permission errors) will now only surface as a generic exit-code error rather than git's actual diagnostic text, in both the override and restore paths. This is a reasonable trade-off given the security fix's goal, but a listeners.stderr buffer could preserve diagnosability without reintroducing the leak — left as a suggestion, not a blocker.
  • Grumpy-coder sub-agent's other two findings (ordering of setSecret vs. exec calls in the new test) were reviewed and judged too minor/speculative to warrant a comment — silent: true is the actual mitigation, setSecret is explicitly documented as secondary defense-in-depth, so ordering has no real security impact.
  • No correctness, concurrency, or completeness issues found; the fix covers both write (overridePersistedExtraheader) and restore (restorePersistedExtraheader) paths, matching the PR's stated scope.
> 🔎 *Code quality review by [PR Code Quality Reviewer](https://github.com/github/gh-aw/actions/runs/31030724381)* · auto · 43.6 AIC · ⌖ 4.22 AIC · ⊞ 7.9K > Comment /review to run again

// where it would otherwise be captured in uploaded safe-output artifacts.
if (cwd) {
await exec.exec("git", ["config", "--local", "--replace-all", `http.${normalizedUrl}/.extraheader`, authHeader], { cwd });
await exec.exec("git", ["config", "--local", "--replace-all", `http.${normalizedUrl}/.extraheader`, authHeader], { cwd, silent: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

silent: true also suppresses stderr on failure, so a future git config failure here (e.g. lock file contention, permission denied) will surface only a generic "process exited with code N" instead of git's actual error text.

💡 Details

@actions/exec's silent option suppresses both stdout and stderr echoing to the console; none of these four exec.exec calls (this one, line 209, and the two/four calls in restorePersistedExtraheader at lines 250/252/255/257) register listeners.stderr or listeners.errline to capture output before it's silenced. Previously, a failed --replace-all/--add would print git's real stderr (e.g. permission denied, index.lock present) to the workflow log, aiding debugging. Now the thrown error from exec.exec will just be The process exited with code N, and restorePersistedExtraheader's catch block only logs getErrorMessage(err) — which won't contain the underlying git diagnostic. This is an acceptable trade-off for the security fix, but consider adding a listeners.stderr buffer (safe, since it captures raw output for programmatic use rather than echoing it) so the actual git error can still be logged via core.warning on failure, without reintroducing the leak.

let stderrBuf = "";
await exec.exec("git", ["config", ...], {
  cwd,
  silent: true,
  listeners: { stderr: (data) => { stderrBuf += data.toString(); } },
});
// on catch: core.warning(`git config failed: ${stderrBuf}`)

Rationale: security fix silences the echoed command line (the actual leak vector), but as written it also silently discards diagnostic stderr that never contained the secret itself — that data loss is avoidable.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test(s): 1 design, 0 implementation, 0 violation(s).

📊 Metrics (1 new/modified test)
Metric Value
Analyzed 1 (Go: 0, JS: 1)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation No (21 test lines / 16 prod lines = 1.31×)
🚨 Violations 0
Test File Classification Issues
should register the derived base64 header as a secret and never echo it via exec git_auth_helpers.test.cjs:223 design_test · behavioral_contract · high_value None

Analysis

The PR adds one focused security test in overridePersistedExtraheader:

  • setSecret masking — asserts mockCore.setSecret is called with the derived base64 value, verifying defense-in-depth runtime masking is wired up.
  • silent: true on all exec calls — loops over every exec.exec call and asserts the options include { silent: true }, directly guarding against the credential-capture regression (command-line token echoed into safe-output artifacts).

This is a high-value design test: if deleted, the exact credential-leak bug this PR fixes would silently regress with no CI signal. The test verifies a user-visible security invariant, not an internal implementation detail.

Several existing tests were also updated to include expect.objectContaining({ silent: true }) where the options object previously used expect.anything(), tightening existing coverage.

Inflation: 21 added test lines vs. 16 production lines (1.31×) — within threshold.
Mocking: exec and core mocked at the external I/O boundary — acceptable for vitest unit tests.
Violations: None.

Verdict

Passed. 0% implementation tests (threshold: 30%). No violations.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 50.1 AIC · ⌖ 8.18 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Triage: #50636

Category: bug | Risk: high | Priority Score: 80/100 (impact 35, urgency 25, quality 20)
Recommended action: fast_track
Security-sensitive fix: prevents credential leakage into CI artifacts. High urgency.

Generated by 🔧 PR Triage Agent · auto · 45.2 AIC · ⌖ 2.9 AIC · ⊞ 8K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please follow up on the latest review feedback on this PR:

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.8 AIC · ⌖ 5.72 AIC · ⊞ 8.3K ·
Comment /souschef to run again

…mask command

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please follow up on the latest review feedback on this PR:

Removed core.setSecret(tokenBase64) from overridePersistedExtraheader — as noted, core.setSecret itself emits ::add-mask::<tokenBase64> to stdout which process_safe_outputs.cjs tees into the raw artifact before runner masking applies. The silent: true exec options remain as the sole protection. Also removed the corresponding setSecret mock and test assertion from git_auth_helpers.test.cjs, and fixed the CI failure in extra_empty_commit.integration.test.cjs (which was failing with core.setSecret is not a function since its mock didn't include setSecret). Commit: the latest push.

Copilot AI requested a review from gh-aw-bot August 5, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Safe-output artifacts can expose base64 CI trigger tokens

4 participants