Skip to content

fix: Safe-outputs file-protection bypass via patch-parser differential#36752

Merged
lpcox merged 5 commits into
mainfrom
fix/patch-parser-file-protection-bypass
Jun 4, 2026
Merged

fix: Safe-outputs file-protection bypass via patch-parser differential#36752
lpcox merged 5 commits into
mainfrom
fix/patch-parser-file-protection-bypass

Conversation

@lpcox
Copy link
Copy Markdown
Collaborator

@lpcox lpcox commented Jun 3, 2026

fix: Safe-outputs file-protection bypass via patch-parser differential

Summary

Fixes a security vulnerability (CVE-class) where a crafted patch could bypass
safe-outputs file protection checks by exploiting parser differences between the
JavaScript patch parser and git am. The JS parser and git am can disagree on
which files a patch touches, so an adversarial patch could pass JS-side
protection checks but write to protected files when applied by git am.

The fix introduces post-apply enforcement: the actual files written to disk
by git am are diffed against the file-protection policy using a git diff
of the pre-apply HEAD ref. Violations cause an immediate HEAD reset and either a
hard failure or a fallback issue, depending on context.


Problem

File protection in safe-outputs was enforced pre-apply by parsing diff --git
headers with a JS parser. A specially crafted patch could include headers that
the JS parser accepted as non-protected, but that git am would apply to
protected paths. This created a parser-differential bypass window.

Additionally, unparseable diff --git headers were previously silently ignored
(fail-open), meaning malformed headers could also be used to evade the check.


Changes

Security fix — post-apply file-protection enforcement

File What changed
actions/setup/js/manifest_file_helpers.cjs Added checkFileProtectionPostApply() — verifies protection policy against files actually written by git am (via git diff <pre-apply-HEAD>). Fail-closed on unparseable diff --git headers. Normalized dot-folder excludes handling.
actions/setup/js/create_pull_request.cjs Captures the HEAD ref before git am, then invokes post-apply protection check against the real written files.
actions/setup/js/push_to_pull_request_branch.cjs Imports checkFileProtectionPostApply and runs it after git am; resets HEAD and creates a fallback issue on violations.

Tests

File What changed
actions/setup/js/manifest_file_helpers.test.cjs Malformed-header test updated to expect a throw (fail-closed). New suites for extractPathsFromPatch security edge cases and checkFileProtectionPostApply regression variants.
actions/setup/js/create_pull_request.test.cjs New test: post-apply check diffs against the correct pre-apply HEAD ref.
actions/setup/js/push_to_pull_request_branch.test.cjs New test: post-apply protected-file detection triggers fallback issue instead of push.

Housekeeping

File What changed
.changeset/patch-fix-patch-parser-file-protection-bypass.md Patch-level changeset entry for this fix.
.github/skills/agentic-workflows/SKILL.md Adds .github/aw/mcp-clis.md to the remote-file load list.

Security posture

  • Before: file protection enforced pre-apply by JS parser (fail-open on bad headers) → bypassable via parser-differential.
  • After: file protection enforced post-apply against ground-truth written files + fail-closed on malformed headers → no parser-differential bypass window.

Breaking changes

None. All changes are internal enforcement logic with no public API surface changes.


Testing

  • Existing unit tests updated to reflect fail-closed behavior.
  • New regression tests cover the specific bypass vectors.
  • No manual steps required; CI covers all new and modified tests.

Generated by PR Description Updater for issue #36752 · sonnet46 2.1M ·

Addresses a security vulnerability where the JS patch parser and git am
can disagree on which files a patch contains, allowing an attacker to
bypass allowed-files/protected-files policies.

Three fixes:

1. Post-apply enforcement (primary): After git am applies a patch and
   BEFORE push, run `git diff --name-only` against the pre-apply commit
   to get the actual files written. Re-check file protection against this
   ground-truth list. This eliminates all parser-differential attacks
   regardless of variant.

2. Fail-closed on unparseable headers: extractPathsFromPatch() now throws
   on any diff --git header where parseable === false, instead of silently
   skipping it. This prevents crafted headers from evading policy checks.

3. Regression tests: Added test cases for unparseable headers, post-apply
   detection of files outside allowed-files, protected paths, and
   top-level dot folders.

Fixes github/agentic-workflows#539

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 3, 2026 23:25
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

✅ smoke-ci: safeoutputs CLI comment + comment-memory run (26919435461)

Generated by 🧪 Smoke CI for issue #36752 ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Comment Memory

CI lights the path
Green checks bloom at dawn
Quiet bots still sing

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

Generated by 🧪 Smoke CI for issue #36752 ·

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens safe-output file-protection enforcement against patch-parser differentials by verifying the actual files written after git am (via git diff --name-only --no-renames <base>..HEAD) and by failing closed when encountering unparseable diff --git headers.

Changes:

  • Add post-apply file-protection verification to push_to_pull_request_branch and create_pull_request flows.
  • Update patch path extraction to throw on unparseable diff --git headers (fail-closed).
  • Add regression/unit tests covering unparseable headers and post-apply protection outcomes.
Show a summary per file
File Description
actions/setup/js/push_to_pull_request_branch.cjs Adds post-apply git diff-based enforcement before pushing to an existing PR branch.
actions/setup/js/create_pull_request.cjs Adds post-apply enforcement after patch apply and before pushing/creating PR.
actions/setup/js/manifest_file_helpers.cjs Makes unparseable diff --git headers fail-closed; adds checkFileProtectionPostApply(actualFiles, config).
actions/setup/js/manifest_file_helpers.test.cjs Adds/updates regression tests for fail-closed parsing and post-apply policy checks.

Copilot's findings

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 7

Comment on lines +315 to +319
if (file.startsWith(".") && file.includes("/")) {
const topFolder = file.split("/")[0];
if (!dotFolderExcludes.includes(topFolder) && !allProtected.includes(file)) {
allProtected.push(file);
}
Comment on lines +709 to +713
it("should respect dot folder excludes", () => {
const result = checkFileProtectionPostApply(
[".changeset/fix.md"],
{ protect_top_level_dot_folders: true, protected_dot_folder_excludes: [".changeset"], protected_files_policy: "deny" }
);
Comment on lines +64 to +66
throw new Error(
`Patch contains unparseable diff --git header (fail-closed): "${entry.headerLine}". ` +
`Cannot verify file-protection policy. Rejecting patch.`
Comment on lines +921 to +924
if (postApplyProtection.action === "fallback") {
protectedFilesForFallback = postApplyProtection.files;
core.warning(`Post-apply: Protected file protection triggered (fallback-to-issue): ${postApplyProtection.files.join(", ")}`);
}
Comment on lines +1748 to +1750
const diffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `origin/${baseBranch}..HEAD`]);
const actualFiles = diffResult.stdout.split("\n").map(f => f.trim()).filter(Boolean);
if (actualFiles.length > 0) {
Comment on lines +906 to +910
const diffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `${rangeBaseRef}..HEAD`], baseGitOpts);
const actualFiles = diffResult.stdout.split("\n").map(f => f.trim()).filter(Boolean);
if (actualFiles.length > 0) {
core.info(`Post-apply verification: ${actualFiles.length} file(s) actually modified`);
const postApplyProtection = checkFileProtectionPostApply(actualFiles, config);
Comment on lines +1748 to +1752
const diffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `origin/${baseBranch}..HEAD`]);
const actualFiles = diffResult.stdout.split("\n").map(f => f.trim()).filter(Boolean);
if (actualFiles.length > 0) {
core.info(`Post-apply verification: ${actualFiles.length} file(s) actually modified`);
const postApplyProtection = checkFileProtectionPostApply(actualFiles, {
@lpcox
Copy link
Copy Markdown
Collaborator Author

lpcox commented Jun 3, 2026

@copilot address review feedback

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 3, 2026

@copilot address review feedback

Addressed in 401b8ca. The update normalizes dot-folder excludes post-apply, sanitizes unparseable header errors, uses the actual pre-apply HEAD for create-pull-request post-apply diffs, enforces fallback-to-issue after post-apply detection in push-to-PR, and adds focused regression coverage for both handlers.

Copilot finished work on behalf of lpcox June 3, 2026 23:50
@lpcox
Copy link
Copy Markdown
Collaborator Author

lpcox commented Jun 4, 2026

@copilot resolve the merge conflicts in this pull request

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 4, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in c892dcf by merging origin/main into this branch and fixing the actions/setup/js/create_pull_request.cjs conflict.

Copilot finished work on behalf of lpcox June 4, 2026 00:18
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

``
@copilot review all comments and address the unresolved review feedback. Please also add a focused differential test and summarize any remaining blockers.

Generated by 👨‍🍳 PR Sous Chef · gpt54mini 3.1M ·

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 4, 2026

@copilot apply copilot-review skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 4, 2026

@copilot apply copilot-review skill

Applied the copilot-review workflow: I re-collected the in-scope review feedback, verified the previously requested fixes are present in 401b8ca and c892dcf, checked the current PR review threads, and reran the targeted JS tests (manifest_file_helpers, patch_path_helpers.integration, push_to_pull_request_branch, create_pull_request), which all passed.

Copilot AI requested a review from pelikhan June 4, 2026 02:10
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

@copilot review all comments and address unresolved review feedback.
Please resolve the remaining open review threads, starting with the fallback-path and diff-base security concerns.

Generated by 👨‍🍳 PR Sous Chef · gpt54mini 7.2M ·

@lpcox lpcox added the smoke label Jun 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🚀 Smoke Antigravity MISSION COMPLETE! Antigravity has spoken. ✨

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🚀 Smoke Gemini MISSION COMPLETE! Gemini has spoken. ✨

Caution

agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.

Details

The threat detection results could not be parsed.

Review the workflow run logs for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🚀 Smoke Pi MISSION COMPLETE! Pi delivered. 🥧

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

✅ All tools validated successfully! Agent Container Smoke Test confirms agent container is ready.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🎬 THE ENDSmoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing...

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Agent Container Tool Check

Tool Status Version
bash 5.2.21
sh available
git 2.54.0
jq 1.7
yq 4.53.2
curl 8.5.0
gh 2.92.0
node 24.16.0
python3 3.13.13
go 1.24.13
java 10.0.300
dotnet 10.0.300

Result: 12/12 tools available ✅

Overall Status: PASS

🔧 Tool validation by Agent Container Smoke Test · sonnet46 349.2K ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Caution

agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.

Details

The threat detection results could not be parsed.

Review the workflow run logs for details.

Smoke Test Results

  • GitHub MCP: ✅
  • Web Fetch: ✅
  • File Writing: ✅
  • Bash Tool: ✅
  • Build gh-aw: ❌

Overall Status: FAIL

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • localhost

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "localhost"

See Network Configuration for more information.

Smoke Gemini — Powered by Gemini ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Comment Memory

Quiet build lights up
Smoke drifts through the cache and logs
Checks settle to green

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔮 The oracle has spoken through Smoke Codex · gpt54mini 5.7M ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🤖 Smoke Test: Claude — Run 26927612883

Core #1-13: ✅ (note: #2 mcpscripts-gh not exposed → used github_pr_query ⚠️)
PR review #14-19: ✅ · #17 resolved a thread ✅ · #20 close-PR ⚠️ skipped

Overall: PARTIAL (run)

Warning

Firewall blocked 6 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • clients2.google.com
  • contentautofill.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "accounts.google.com"
    - "android.clients.google.com"
    - "clients2.google.com"
    - "contentautofill.googleapis.com"
    - "safebrowsingohttpgateway.googleapis.com"
    - "www.google.com"

See Network Configuration for more information.

💥 [THE END] — Illustrated by Smoke Claude · opus48 5.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

💥 Automated smoke test review - all systems nominal!

Warning

Firewall blocked 6 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • clients2.google.com
  • contentautofill.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "accounts.google.com"
    - "android.clients.google.com"
    - "clients2.google.com"
    - "contentautofill.googleapis.com"
    - "safebrowsingohttpgateway.googleapis.com"
    - "www.google.com"

See Network Configuration for more information.

💥 [THE END] — Illustrated by Smoke Claude · opus48 5.1M

/** @typedef {import('./types/handler-factory').HandlerConfig} HandlerConfig */
const { extractDiffGitHeaderEntries } = require("./patch_path_helpers.cjs");

function normalizeDotFolderExcludes(excludes) {
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.

Nice helper — normalizeDotFolderExcludes cleanly trims trailing slashes and filters empties. Consider a brief JSDoc for clarity. (smoke test 26927612883)

return new Set((Array.isArray(excludes) ? excludes : []).map(exclude => String(exclude || "").replace(/\/+$/, "")).filter(Boolean));
}

function formatHeaderLineForError(headerLine) {
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.

formatHeaderLineForError truncation at 200 chars is a sensible guard for log readability. (smoke test 26927612883)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Commit pushed: 32a466c

Generated by Changeset Generator

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Smoke bot done.
PR: fix: Safe-outputs file-protection bypass via patch-parser differential
✅ 1 2 3 4 6 7 8 9 10 12 13 14 15 16
❌ 5 11
Overall: FAIL
Author: @lpcox
Assignees: none

Warning

Firewall blocked 6 domains

The following domains were blocked by the firewall during workflow execution:

  • accounts.google.com
  • android.clients.google.com
  • clients2.google.com
  • contentautofill.googleapis.com
  • safebrowsingohttpgateway.googleapis.com
  • www.google.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "accounts.google.com"
    - "android.clients.google.com"
    - "clients2.google.com"
    - "contentautofill.googleapis.com"
    - "safebrowsingohttpgateway.googleapis.com"
    - "www.google.com"

See Network Configuration for more information.

📰 BREAKING: Report filed by Smoke Copilot · gpt54 6.3M ·

@lpcox lpcox merged commit cba0603 into main Jun 4, 2026
10 of 13 checks passed
@lpcox lpcox deleted the fix/patch-parser-file-protection-bypass branch June 4, 2026 04:03
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.

4 participants