# CI shell injection via unquoted interpolation

Untrusted refs, names, paths or config values placed unquoted into shell or CLI commands in CI

_Area: Software supply chain and CI/CD. Seen in 7 disclosed reports; 2 confirmed bounties: $500 and $4,000; latest 2026-10-21. Updated 2026-10-23._

## How it works
A CI script or tool builds a shell or CLI command by pasting a value it does not control (issue comment text, branch or directory name, env var name, config setting, environment path) directly into the command string. The value is treated as syntax, not data. The broken assumption is that the value is a harmless token: a leading `--` becomes a CLI option, a quote or backslash breaks out of a quoted literal, and `$(...)` or backticks run as command substitution. The same flaw appears when a quoting helper is applied to most inputs but skipped for one field, or when a quoter for one shell is reused for another with different rules.

## Where to show up
- `exec(`/`execSync`/`sh -c` with template literals or string concatenation containing comment-derived values, hashes, labels, usernames [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md)
- Unquoted `${{ inputs.* }}` or path variables in workflow `run:` steps and action code [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md)
- Generic POSIX quoter (`shlex.quote`) emitting scripts for fish, zsh, csh [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c947-3pg5-gm8q.md)[E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q.md)
- Config settings that select an interpreter or executable, surviving a "local untrusted" strip [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-g74g-rg72-j2p3.md)
- A `shell.escape`-style helper applied to image/command/path but not env var names, labels, keys [E5](https://bugbountylibrary.com/r/h1%3A2221404.md)
- Multi-pass template substitution that re-scans already-quoted text [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-8rjx-v5ww-45pp.md)
- Escaping gated on a shell-version check (`BASH_VERSION`) leaving other shells raw [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q.md)

## How to review
Trace every value from an untrusted source to each shell/CLI interpolation point. The invariant: untrusted data must reach the command as a single argument, never as syntax. Check that it is passed via argument arrays or a correct quoter for the actual target shell, that leading `-`/`--` cannot be parsed as options, and that no later substitution pass rewrites quoted content. Enumerate all fields of a sink class, not just the ones already fixed.

## Variants seen
- Regex captured arbitrary non-whitespace plus missing auth check, value into `exec` [E1](https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-vscode-write-access.md)
- Arbitrary `source_dir` into unquoted `tar --directory`, leading `--` as option [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.md)
- POSIX quoter used for fish, backslash-before-quote escapes [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-c947-3pg5-gm8q.md)
- Local config overrides shell interpreter args after partial fix [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-g74g-rg72-j2p3.md)
- Env var name skipped by escape helper [E5](https://bugbountylibrary.com/r/h1%3A2221404.md)
- Escaping only under bash; zsh `PROMPT_SUBST` expands PS1 [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q.md)
- Per-key substitution re-expands placeholder inside quoted value [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-8rjx-v5ww-45pp.md)

## Why fixes fail
Fixes were per-field denylists or shell-gated: marking only some settings `global_only` left interpreter-selecting fields open [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-g74g-rg72-j2p3.md); escaping behind `BASH_VERSION` left zsh unescaped [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q.md); per-key substitution re-scanned quoted text [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-8rjx-v5ww-45pp.md).

## Impact and severity
Led to command execution on CI runners and worker hosts, and write access to a major repo [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)[E5](https://bugbountylibrary.com/r/h1%3A2221404.md). Severity is high where the runner holds release or signing credentials; worse when any user can trigger the pipeline or when the command still succeeds silently [E2](https://bugbountylibrary.com/r/blog%3Ablog-nietaanraken-nl-posts-github-pages-command-injection.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/ghsa%3AGHSA-c947-3pg5-gm8q.md
- E4: https://bugbountylibrary.com/r/ghsa%3AGHSA-g74g-rg72-j2p3.md
- E5: https://bugbountylibrary.com/r/h1%3A2221404.md
- E6: https://bugbountylibrary.com/r/ghsa%3AGHSA-5vjq-rrrf-7h2q.md
- E7: https://bugbountylibrary.com/r/ghsa%3AGHSA-8rjx-v5ww-45pp.md
