Summary
Patch.ensure_targets_within! — added by PR #22466 to constrain a downloaded patch's targets to the staged source tree — only inspects ---/+++/*** target lines. patch also derives a target from an Index: header. A server-supplied external patch whose only target line is Index: a/../<existing-path> (no ---/+++ lines) passes the validator, and on macOS (Apple patch) the write lands at that out-of-tree path. This is an incomplete-protection / documented-protection-bypass defect in Homebrew's own validator (CWE-693). On a default brew install the resulting write is contained by Homebrew's build sandbox to brew's write allowlist (it is not a path to $HOME/code execution by default — see Impact).
Details
- Validator gap:
Library/Homebrew/patch.rb:48 regex /\A(?:---|\+\+\+|\*\*\*)\s+(\S+)/ inspects only ---/+++/***. An Index:-only patch is not inspected, so ensure_targets_within! passes it. (The #22466 test suite, test/patch_spec.rb, only exercises ---/+++/***.)
- Sink / server vector:
external_patch.rb:74-77 calls the validator then patch -g 0 -f -p1 on the same bytes — this is the server-supplied path (patch do url "..." end). (embedded_patch.rb:46 shares the validator but its content is formula-author-controlled, i.e. trusted-tap text, not a server vector.)
- Platform /
patch behavior: on macOS, Apple patch 2.0-12u11 (the patch brew invokes) honors the Index: header and does not refuse .. targets, so the out-of-tree write fires. On Linux, GNU patch 2.7.6's post-CVE-2018-1000156 hardening refuses ../absolute targets, so the same bypass is bounded. The proven macOS write is via the Index: header specifically; diff --git/rename-only patches did not write on Apple patch (they are additional validator gaps the fix should also close, relevant to Linux/GNU target derivation, not a macOS write vector).
- Write constraint: a
1c1/replace hunk requires the out-of-tree target to pre-exist (matching context), but a creation hunk (@@ -0,0 +1 @@) under an Index:-only header creates a new out-of-tree file — and any missing parent directories — on Apple patch 2.0-12u11 (verified: Index: a/../bypass-created.txt → patching file '../bypass-created.txt', exit 0; Index: a/../newdir/deep.txt → newdir/ created, exit 0). GNU patch 2.7.6 refuses both via its CVE-2018-1000156 hardening.
- Containment (corrected from an earlier draft): a default
brew install runs formula.patch inside Homebrew's build sandbox (formula_installer.rb use_sandbox?("building"); macOS sandbox-exec is available by default). The default sandbox does deny_read_home and does not include $HOME in its write allowlist (the allowlist is HOMEBREW_TEMP/CACHE, build logs, ~/Library/Developer + the swiftpm cache, and the Cellar). So the out-of-tree write is confined to that allowlist — it can write within the shared HOMEBREW_CACHE, HOMEBREW_PREFIX/etc and /var, build logs, and this formula's own Cellar rack (an integrity write across brew's own build authority; allow_write_cellar scopes Cellar writes to this formula's rack, while etc/var/cache are shared cross-formula), but not $HOME. Reaching $HOME (and a shell-rc/LaunchAgent code-exec chain) additionally requires brew install --interactive (formula_installer.rb adds allow_write_path(Dir.home) only when interactive?) or a non-default unsandboxed configuration.
- This is Homebrew's defect, not
patch(1)'s: ensure_targets_within! exists precisely to bound server-supplied patch targets; missing the Index:/diff --git/rename headers is an incomplete protection mechanism, not upstream patch behavior to report elsewhere.
The reporter recommends extending ensure_targets_within! to also parse the target from Index:, diff --git, and rename from/rename to headers — the same filenames patch will act on. Final severity and remediation are the PSRT's to decide.
PoC
- Validator bypass (platform-agnostic): the real HEAD
Patch.ensure_targets_within! returns without raising on an Index: a/../escape.txt-only patch (no ---/+++); a --- a/../../x form is rejected (the control inspects), and a validator extended to parse Index: catches the payload.
- macOS out-of-tree write (Apple patch 2.0-12u11, macOS 26.5.1), bare
patch (no sandbox), non-destructive temp dir: running brew's exact patch -g 0 -f -p1 from a staged dir on an Index: a/../escape.txt body overwrote a pre-existing file one level outside the staged dir (patching file '../escape.txt', exit 0). This demonstrates the patch-level out-of-tree write; the real brew install adds the sandbox containment described above, so on the default path the reachable targets are the sandbox write-allowlist, not $HOME.
Impact
Integrity write. A server-supplied patch body defeats the #22466 staged-tree containment and writes attacker-controlled content to a file (existing or newly created) outside the staged source tree. On a default macOS brew install the write is confined by Homebrew's build sandbox to brew's write allowlist: the shared HOMEBREW_CACHE (global download cache), HOMEBREW_PREFIX/etc and /var (shared installed-formula config/state), build logs, and this formula's own Cellar rack — an integrity write across brew's own build authority, not $HOME. A $HOME write (and the shell-rc/LaunchAgent code-exec chain) requires --interactive or an unsandboxed config and is not the default impact.
Delivery gate. Official homebrew-core external patches carry a sha256; a MITM or compromised host swapping the body raises ChecksumMismatchError before patch runs. So the attacker-chosen body requires either a checksumless external patch (not used in homebrew-core) or control of the formula. The in-scope hook is the documented-protection bypass, and the PSRT may reasonably treat this as defense-in-depth behind the checksum gate.
Who is impacted. A user installing a formula whose external patch is attacker-influenced and unchecksummed (or via a tap/local edit). CWE-693 (Protection Mechanism Failure), CWE-22 (Path Traversal).
AI/LLM use disclosure
This finding was researched and validated with AI/LLM assistance; the reporter verified its correctness against the source and the recorded run, and takes full responsibility for it.
- How: an AI agent system (Anthropic Claude, via Claude Code) performed native source-code review to surface the candidate, ran a live validator-bypass and macOS
patch PoC, and ran an adversarial multi-lens review against the real source that corrected an earlier overclaim (the sandbox blocks the default-path $HOME/code-exec chain) and the delivery framing (sha256 gate). AI output was treated as a fallible first draft and human-verified.
- When: 2026-06-26 – 2026-06-29, against
master HEAD c0ed315adb.
- Where: in the research (discovery + PoC + adversarial review) and in drafting this write-up.
- Prompts: the operative instruction was — "find every 0-day from the latest HEAD; every finding must have a PoC/wire-proof with recorded reachability stdout; conform with the security model and vuln-filing conventions; use disposable VMs within policy to fan out." For this finding: call the real
ensure_targets_within! on an Index:-only traversal patch and assert it passes; run brew's patch -g 0 -f -p1 on macOS with a .. Index: target and record the write; then adversarially verify scope against the sandbox and checksum gates. Full prompt/agent logs available to the PSRT on request.
Summary
Patch.ensure_targets_within!— added by PR #22466 to constrain a downloaded patch's targets to the staged source tree — only inspects---/+++/***target lines.patchalso derives a target from anIndex:header. A server-supplied external patch whose only target line isIndex: a/../<existing-path>(no---/+++lines) passes the validator, and on macOS (Applepatch) the write lands at that out-of-tree path. This is an incomplete-protection / documented-protection-bypass defect in Homebrew's own validator (CWE-693). On a defaultbrew installthe resulting write is contained by Homebrew's build sandbox to brew's write allowlist (it is not a path to$HOME/code execution by default — see Impact).Details
Library/Homebrew/patch.rb:48regex/\A(?:---|\+\+\+|\*\*\*)\s+(\S+)/inspects only---/+++/***. AnIndex:-only patch is not inspected, soensure_targets_within!passes it. (The #22466 test suite,test/patch_spec.rb, only exercises---/+++/***.)external_patch.rb:74-77calls the validator thenpatch -g 0 -f -p1on the same bytes — this is the server-supplied path (patch do url "..." end). (embedded_patch.rb:46shares the validator but its content is formula-author-controlled, i.e. trusted-tap text, not a server vector.)patchbehavior: on macOS, Applepatch 2.0-12u11(thepatchbrew invokes) honors theIndex:header and does not refuse..targets, so the out-of-tree write fires. On Linux, GNU patch 2.7.6's post-CVE-2018-1000156 hardening refuses../absolute targets, so the same bypass is bounded. The proven macOS write is via theIndex:header specifically;diff --git/rename-only patches did not write on Apple patch (they are additional validator gaps the fix should also close, relevant to Linux/GNU target derivation, not a macOS write vector).1c1/replace hunk requires the out-of-tree target to pre-exist (matching context), but a creation hunk (@@ -0,0 +1 @@) under anIndex:-only header creates a new out-of-tree file — and any missing parent directories — on Applepatch 2.0-12u11(verified:Index: a/../bypass-created.txt→patching file '../bypass-created.txt', exit 0;Index: a/../newdir/deep.txt→newdir/created, exit 0). GNU patch 2.7.6 refuses both via its CVE-2018-1000156 hardening.brew installrunsformula.patchinside Homebrew's build sandbox (formula_installer.rbuse_sandbox?("building"); macOSsandbox-execis available by default). The default sandbox doesdeny_read_homeand does not include$HOMEin its write allowlist (the allowlist is HOMEBREW_TEMP/CACHE, build logs,~/Library/Developer+ the swiftpm cache, and the Cellar). So the out-of-tree write is confined to that allowlist — it can write within the sharedHOMEBREW_CACHE,HOMEBREW_PREFIX/etcand/var, build logs, and this formula's own Cellar rack (an integrity write across brew's own build authority;allow_write_cellarscopes Cellar writes to this formula's rack, while etc/var/cache are shared cross-formula), but not$HOME. Reaching$HOME(and a shell-rc/LaunchAgent code-exec chain) additionally requiresbrew install --interactive(formula_installer.rbaddsallow_write_path(Dir.home)only wheninteractive?) or a non-default unsandboxed configuration.patch(1)'s:ensure_targets_within!exists precisely to bound server-supplied patch targets; missing theIndex:/diff --git/renameheaders is an incomplete protection mechanism, not upstreampatchbehavior to report elsewhere.The reporter recommends extending
ensure_targets_within!to also parse the target fromIndex:,diff --git, andrename from/rename toheaders — the same filenamespatchwill act on. Final severity and remediation are the PSRT's to decide.PoC
Patch.ensure_targets_within!returns without raising on anIndex: a/../escape.txt-only patch (no---/+++); a--- a/../../xform is rejected (the control inspects), and a validator extended to parseIndex:catches the payload.patch(no sandbox), non-destructive temp dir: running brew's exactpatch -g 0 -f -p1from a staged dir on anIndex: a/../escape.txtbody overwrote a pre-existing file one level outside the staged dir (patching file '../escape.txt', exit 0). This demonstrates the patch-level out-of-tree write; the realbrew installadds the sandbox containment described above, so on the default path the reachable targets are the sandbox write-allowlist, not$HOME.Impact
Integrity write. A server-supplied patch body defeats the #22466 staged-tree containment and writes attacker-controlled content to a file (existing or newly created) outside the staged source tree. On a default macOS
brew installthe write is confined by Homebrew's build sandbox to brew's write allowlist: the sharedHOMEBREW_CACHE(global download cache),HOMEBREW_PREFIX/etcand/var(shared installed-formula config/state), build logs, and this formula's own Cellar rack — an integrity write across brew's own build authority, not$HOME. A$HOMEwrite (and the shell-rc/LaunchAgent code-exec chain) requires--interactiveor an unsandboxed config and is not the default impact.Delivery gate. Official homebrew-core external patches carry a
sha256; a MITM or compromised host swapping the body raisesChecksumMismatchErrorbeforepatchruns. So the attacker-chosen body requires either a checksumless external patch (not used in homebrew-core) or control of the formula. The in-scope hook is the documented-protection bypass, and the PSRT may reasonably treat this as defense-in-depth behind the checksum gate.Who is impacted. A user installing a formula whose external patch is attacker-influenced and unchecksummed (or via a tap/local edit). CWE-693 (Protection Mechanism Failure), CWE-22 (Path Traversal).
AI/LLM use disclosure
This finding was researched and validated with AI/LLM assistance; the reporter verified its correctness against the source and the recorded run, and takes full responsibility for it.
patchPoC, and ran an adversarial multi-lens review against the real source that corrected an earlier overclaim (the sandbox blocks the default-path$HOME/code-exec chain) and the delivery framing (sha256 gate). AI output was treated as a fallible first draft and human-verified.masterHEADc0ed315adb.ensure_targets_within!on anIndex:-only traversal patch and assert it passes; run brew'spatch -g 0 -f -p1on macOS with a..Index:target and record the write; then adversarially verify scope against the sandbox and checksum gates. Full prompt/agent logs available to the PSRT on request.