flawopen.com/GitHub Actions security/Workflow script injection
How does GitHub Actions script injection happen, and how do you prevent it?
A workflow can turn data into shell syntax by inserting an issue title or pull request field into a script before Bash starts. It is like a label printer that mistakes a name for instructions. Pass the value through an environment variable and quote it when using it as data; then Bash receives the value separately instead of parsing it as part of the script. The impact still depends on the job's token, secrets, runner, and trigger.
${{ ... }} before the shell runs. In a run: block this can splice input into generated script source.env: passes it as process data. Quote the shell expansion and avoid eval or constructing another script.Root cause: data is inserted before shell parsing
GitHub expands expressions before launching the shell for a run step. If an untrusted title is interpolated directly into the script, its characters can change the shell program rather than remain data. A value passed through env: is separate from the script text; quoting its expansion preserves it as one argument for ordinary commands. This does not make later use with eval, another interpreter, or an unsafe downstream tool safe. GitHub documents untrusted event fields as attacker-controlled input.
# UNSAFE: event data becomes part of the generated shell script
name: Issue Triage
on:
issues:
types: [opened]
permissions:
contents: read
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Print issue title
run: |
printf 'Title: %s\n' "${{ github.event.issue.title }}"
# Safer: pass the event value through the environment
name: Issue Triage
on:
issues:
types: [opened]
permissions:
contents: read
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Print issue title
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
printf 'Title: %s\n' "$ISSUE_TITLE"
Exploit path at a glance
A user controls an event field
For example, a public issue title, pull request title, or branch name becomes input to a workflow.
The workflow splices it into shell source
A direct ${{ github.event... }} expression inside run: is expanded before Bash parses the script.
The shell interprets the altered program
If the value changes shell syntax, commands can run with the job process identity. No injection occurs when the field stays data and is used as a quoted argument.
The job’s permissions set the impact
A compromised job can access only resources exposed to that run, including its effective GITHUB_TOKEN, explicitly available secrets, and reachable runner resources. Issue-triggered workflows and pull_request_target require careful privilege review.
Hardening checklist
- ✓Never interpolate untrusted
github.eventvalues directly into arun:script. Pass them throughenv:, then use quoted expansions such as"$ISSUE_TITLE". - ✓Keep untrusted values out of
eval, generated scripts, shell command strings, and tools that interpret their arguments as code or options. - ✓Set a restrictive workflow or job
permissions:block and grant write scopes only to the job that requires them. - ✓Treat
pull_request_targetandworkflow_runas privileged boundaries. Do not check out, build, or execute untrusted pull request code or artifacts in that context. - ✓Do not write untrusted multiline data to
GITHUB_ENVorGITHUB_PATH. GitHub blocks overwritingGITHUB_*andRUNNER_*variables, but other values can still affect later steps. - ✓Pin third-party actions to reviewed full commit SHAs, use ephemeral isolated runners for untrusted jobs, and enable CodeQL scanning for Actions workflows.
Primary sources
- GitHub Docs — Script injections
- GitHub Docs — Secure use reference
- GitHub Docs — Securely using pull_request_target
- GitHub Docs — Workflow commands and GITHUB_ENV
- GitHub Docs — GITHUB_TOKEN permissions