GitHub: command injection
Summary
gh-aw already treats an attacker-written .git/ as a threat.
setup_cache_memory_git.sh:143-147:
> Git hook files under .git/hooks/ are preserved in the cache but are NOT
> tracked by git (git add -A ignores .git/). A compromised agent run could
> write executable hooks (e.g. post-checkout, post-merge) that would be restored
> from cache and executed on the host runner before the AWF sandbox is
> established. Remove all non-sample hook files immediately after cache restore
> to prevent this.
The mitigation is implemented and it works. It covers hooks only. .git/config
is restored from the same cache, is excluded from every sanitization pass, and
can define a filter driver: a shell command git runs during checkout, merge and
add. The script runs checkout at :185 and merge at :211.
The result is that the attacker capability the hook cleanup is built to contain
also gives command execution on the runner host, outside the AWF sandbox, with
no execute bit required. It persists across subsequent restores from that cache
namespace until the entry ages out or is replaced.
Verified on main at commit b2ef1f3 (2026-08-12). First verified at bdfdb8e
(2026-06-24, ~v0.80.9). Every line referenced below is unchanged across that
range.
Details
Locations, in actions/setup/sh/setup_cache_memory_git.sh unless noted:
:143-150hook neutralization:...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| github/gh-aw Product | < v0.87.0 | v0.87.0 |
Details and references
### Summary gh-aw already treats an attacker-written `.git/` as a threat. `setup_cache_memory_git.sh:143-147`: > Git hook files under `.git/hooks/` are preserved in the cache but are NOT > tracked by git (`git add -A` ignores `.git/`). A compromised agent run could > write executable hooks (e.g. post-checkout, post-merge) that would be restored > from cache and executed on the host runner before the AWF sandbox is > established. Remove all non-sample hook files immediately after cache restore > to prevent this. The mitigation is implemented and it works. It covers hooks only. `.git/config` is restored from the same cache, is excluded from every sanitization pass, and can define a filter driver: a shell command git runs during checkout, merge and add. The script runs checkout at `:185` and merge at `:211`. The result is that the attacker capability the hook cleanup is built to contain also gives command execution on the runner host, outside the AWF sandbox, with no execute bit required. It persists across subsequent restores from that cache namespace until the entry ages out or is replaced. Verified on `main` at commit `b2ef1f3` (2026-08-12). First verified at `bdfdb8e` (2026-06-24, ~v0.80.9). Every line referenced below is unchanged across that range. ### Details Locations, in `actions/setup/sh/setup_cache_memory_git.sh` unless noted: - `:143-150` hook neutralization: the comment above, and `find .git/hooks -type f ! -name '*.sample' -delete` - `:77`, `:173` `git config core.hooksPath /dev/null` - `:185` `git checkout -q "$INTEGRITY"` - `:211` `git merge "$level" -X theirs --no-edit -m "merge-from-$level" -q` - `:234`, `:240`, `:283` every sanitization pass is scoped `find . -not -path './.git/*' …` - `actions/setup/sh/commit_cache_memory_git.sh:~40` `git add -A` on save - `actions/setup/js/validate_memory_files.cjs:51-53` post-agent validation skips `.git` - `pkg/workflow/cache_integrity.go:188-223` cache key construction - `pkg/workflow/cache.go:467-490` restore-key derivation Seeding-side locations (see "Seeding" under Impact): - `pkg/workflow/claude_tools.go:396-408` unscoped `Write` appended whenever `safe-outputs` is declared; `:260-274` the scoped `Write(<cacheDir>/*)` grants the guard there fails to see - `pkg/workflow/codex_engine.go:266-270` `--dangerously-bypass-approvals-and-sandbox` when the firewall is enabled; `:284` `features.shell_tool=false` - `pkg/workflow/codex_engine.go:67`, `pkg/workflow/agentic_engine.go:150-154` shell-only scope of the bash-disable mapping - `.github/workflows/ai-moderator.lock.yml:527`, `:529`, `:537`, `:899` step order; `:926` `--mount /tmp/gh-aw:/tmp/gh-aw:rw`; `:927` the codex invocation - `.github/workflows/ai-moderator.md:216` the instruction to write into the cache-memory directory under `bash: false` **The gap.** The three sanitization passes at `:234` (symlink delete), `:240` (execute-bit strip) and `:283` (extension allowlist) are all scoped `-not -path './.git/*'`. They are working-tree controls, so the exclusion is deliberate, but it leaves `.git/config` restored byte-for-byte and outside the `GH_AW_ALLOWED_EXTENSIONS` allowlist that covers everything else the agent writes. `core.hooksPath=/dev/null` closes hooks and nothing else: a filter driver is invoked through the config, not through the hooks path. Git runs the driver via `sh -c`, so the file it operates on needs no execute bit, and the `chmod a-x` at `:240` does not help. That is why this applies on Linux GitHub-hosted runners. **The primitive.** A prior run plants: ``` # .git/config [filter "p"] smudge = sh -c '<command>' # .git/info/attributes * filter=p ``` Both paths are inside `.git/`. On the next run, `git checkout -q "$INTEGRITY"` (`:185`) materializes a file matching the attributes pattern, git invokes the smudge driver, and the command runs on the host. `git add -A` ignores `.git/`, so n