PATTERN Cited by 1 source
Environment-variable interpolation for bash¶
Problem:
GitHub Actions script injection — attacker-controllable
${{ github.event.* }} fields (PR title, branch name,
filenames, issue bodies) get string-interpolated into a
shell script before the shell parses it. Shell
metacharacters in the attacker's input execute as commands.
Pattern: Route attacker-controllable fields through an
env: block. Reference them as "$VAR" in the bash
snippet. Shell-level quoting + one layer of indirection
disarms the injection primitive.
Shape¶
Vulnerable:
Input PR_TITLE="; rm -rf / #" expands mid-parse into a
command-chaining payload.
Safe:
- name: Handle PR
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
echo "The PR title is: $TITLE"
Input PR_TITLE="; rm -rf / #" is set as an environment
variable — when bash reads $TITLE, it receives the literal
string, which it treats as data (the value of an
environment variable), not as code (a shell expression).
Quoting "$TITLE" further protects against word-splitting if
the string contains spaces.
This is GitHub's recommended mitigation.
Why this works¶
The key distinction is when the string is expanded:
${{ ... }}expressions are expanded by GitHub Actions before bash starts. The attacker's string becomes part of the bash script source; shell metacharacters in the string are interpreted by the parser.$VARin bash is expanded by bash after the script is parsed. The attacker's string is always a single data value; shell metacharacters inside it are not re-parsed.
Quote the variable reference ("$TITLE") and the data is
also protected from word-splitting / glob expansion.
Limits¶
- Doesn't defend against
jq/yq/python/node-within-run misinterpretation — those tools have their own injection surfaces. - Doesn't help if the attacker controls a filename and
your step iterates over filenames with
for f in $FILESwhere$FILEScomes from a command-substitution that embeds the filename literally. In that case, use arrays and"${FILES[@]}"properly, or process filenames one-by-one viaxargs -0/ null-separated lists. - Datadog's 2026-02-27
datadog-iac-scannerincident exploited this — the vulnerable workflow did: The filename was the attacker-controlled field; it contained${IFS}-obfuscated command substitution that expanded when bash parsed the line. The fix shape is the same: routefilesthroughenv:and reference as"$FILES".
Related¶
- concepts/github-actions-script-injection — the class this pattern defeats.
- systems/github-actions — the substrate.
- patterns/untrusted-input-via-file-not-prompt — the LLM-prompt analogue (write to file, then read) that composes with this pattern in defensive workflows.
- patterns/org-wide-github-rulesets — containment hardening that limits blast radius when injection defences miss.
Seen in¶
- sources/2026-03-09-datadog-when-an-ai-agent-came-knocking
— Datadog's reference workflow template uses this pattern
throughout; the
datadog-iac-scannerattack was enabled by its absence.