# Untrusted PR metadata into shell

Attacker-set PR, issue or comment fields expanded into run/script blocks or CLI args as code

_Area: Software supply chain and CI/CD. Seen in 8 disclosed reports; latest 2026-10-21. Updated 2026-10-23._

## How it works
GitHub Actions evaluates `${{ }}` expressions before the shell or JS engine parses the step. When a workflow puts an attacker-influenced value (PR title, branch name, comment body, issue title) directly into a `run:` or `script:` block, that value becomes source code, not data. Quoting does not help here: expansion happens first, so a quote or metacharacter in the value breaks out of whatever quoting the author wrote [E3](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md)[E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md)[E8](https://bugbountylibrary.com/r/h1%3A2471956.md). The same expansion-before-parse problem applies to JS `script:` steps [E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md). A related but distinct sink is a user-controlled value passed as a CLI argument, where a leading `--` turns a path into an option [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md); there quoting/escaping does matter, and the fix is argument validation. The broken assumption throughout is that "this field is just text."

## Where it shows up
- `${{ github.event.pull_request.title }}`, `head_ref`, `github.event.comment.body`, `github.event.issue.title` inside `run:` [E3](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md)[E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md)[E5](https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-inject.md)[E8](https://bugbountylibrary.com/r/h1%3A2471956.md)
- `pull_request_target` workflows that read PR fields and hold a write token [E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md)
- Unquoted `--directory ${{ inputs.path }}`-style CLI interpolation [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md)
- Comment-command parsers with a loose regex and no author allowlist (sibling sink: captured value reaches `exec`) [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md)
- A PR that replaced a safe `env:` + `jq --arg` pattern with direct interpolation [E3](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md)
- Escape helpers applied to most inputs but skipped for one field (env var names) [E7](https://bugbountylibrary.com/r/h1%3A2221404.md)

## How to review
Trace every `${{ }}` in a `run:`/`script:` block back to its source. The invariant: no value an outside user can set may reach shell/JS/CLI parsing as code. Safe forms pass the value through `env:` and reference `"$VAR"` quoted, or use `jq --arg`; unsafe forms inline the expression [E3](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md)[E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md). When a codebase escapes most inputs via a helper, enumerate every interpolation point and check which ones skip it [E7](https://bugbountylibrary.com/r/h1%3A2221404.md). For CLI args, confirm the value cannot start with `-` [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md). For comment parsers, check the captured group is constrained and the handler enforces an author allowlist [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md).

## Variants seen
- Comment-command parser with loose regex and missing auth check [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md)
- Unquoted path input becoming a CLI flag [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md)
- Safe pattern regressed to direct interpolation in a PR [E3](https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md)
- Branch name and title into both Bash and JS steps [E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md)
- Comment body into `run:` with `GITHUB_TOKEN` in scope, leading to forged signed release [E5](https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-inject.md)
- Escape helper skipped for env var names in a podman command line [E7](https://bugbountylibrary.com/r/h1%3A2221404.md)
- PR title into a double-quoted shell command [E8](https://bugbountylibrary.com/r/h1%3A2471956.md)

## Why fixes fail
A per-field denylist fix missed sibling settings of the same sink class [E7](https://bugbountylibrary.com/r/h1%3A2221404.md).

## Impact and severity
High: write access to microsoft/vscode [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md), code execution on pypi.org [E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md), a forged signed release shipped to PyPI/GHCR [E5](https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-inject.md). Worse when the token is write-scoped, the repo auto-deploys from main, or the publish pipeline trusts the injected commit [E4](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md)[E5](https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-inject.md).

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md
- E2: https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md
- E3: https://bugbountylibrary.com/r/blog%3Awiz-io-blog-red-agent-snowflake-copilot-cicd-bug.md
- E4: https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-pypi-potential-remote-code-execution-en.md
- E5: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-inject.md
- E6: https://bugbountylibrary.com/r/ghsa%3AGHSA-c947-3pg5-gm8q.md
- E7: https://bugbountylibrary.com/r/h1%3A2221404.md
- E8: https://bugbountylibrary.com/r/h1%3A2471956.md
