GHA Security Review
Review GitHub Actions workflows like an attacker would. Only report issues that can be reached from a realistic external trigger.
When to Use
- Reviewing
.github/workflows/*.ymlor.github/workflows/*.yamlchanges before merge - Auditing a repository that runs PR, issue comment, or reusable workflow automation
- Checking whether workflow files expose secrets, write tokens, or command execution
- Investigating CI/CD compromise paths involving fork PRs or untrusted workflow inputs
When NOT to Use
| Instead of gha-security-review | Use |
|---|---|
| Broad application security review | security-scan |
| General repository trust scorecard | evaluate-repository |
| Threat modeling a system architecture | threat-model-analyst |
Threat Model
Assume an attacker does not have repository write access.
They can:
- open pull requests from forks
- create issues
- comment on issues or PRs
- control PR titles, branch names, commit content in forked code, and comment bodies
They cannot:
- push to protected branches
- trigger
workflow_dispatchmanually - modify repository secrets directly
If exploitation requires write access, do not report it as an in-scope finding.
Workflow
1. Map the workflow attack surface
Review:
.github/workflows/*.ymlor.github/workflows/*.yamlaction.ymloraction.yaml- local reusable actions under
.github/actions/ - config or scripts loaded by workflows such as
AGENTS.md,CLAUDE.md,Makefile, and shell scripts
Start by listing relevant files:
git --no-pager ls-files --cached --others --exclude-standard |
Select-String "^(\\.github/(workflows|actions)/.+|scripts/.+|.+\\.sh|action\\.ya?ml|Makefile|AGENTS\\.md|CLAUDE\\.md)$" |
ForEach-Object { $_.Line }
2. Classify triggers first
For each workflow, identify which external triggers matter:
pull_request_targetpull_requestissue_commentworkflow_callpushscheduleworkflow_dispatch
Only continue down exploit paths that fit the threat model above.
3. Check the high-signal vulnerability classes
3-A. Pwn Request
Look for pull_request_target combined with checkout or execution of fork-controlled code.
git --no-pager grep -n "pull_request_target|actions/checkout|github.event.pull_request.head" -- ".github/workflows"
Report when all three are true:
- external fork PR can trigger the workflow
- the workflow checks out fork content or local actions from that content
- a
run:step or action executes attacker-controlled code
3-B. Expression Injection
Look for attacker-controlled ${{ ... }} expressions inside run: blocks.
git --no-pager grep -n "\${{.*}}" -- ".github/workflows"
Safe patterns to not report:
- numeric-only values like PR numbers
${{ }}inif:conditions${{ }}inwith:inputs${{ secrets.* }}by itself
3-C. Unauthorized Command Execution
Review issue_comment workflows that parse slash commands or bot commands.
Check:
- whether
author_associationis validated - whether any GitHub user can trigger the command
- whether the command body or arguments land in a
run:block unsafely
3-D. Credential Escalation
Look for elevated credentials exposed to untrusted execution contexts:
- PATs
- deploy keys
- repo write tokens
- secrets passed into fork-reachable jobs
3-E. Config Poisoning
Flag workflows that load attacker-controlled config from PR code:
AGENTS.mdCLAUDE.md.cursorrulesMakefile- shell scripts or helper config checked out from the PR
3-F. Supply Chain and Permissions
Check for:
- unpinned third-party actions
- broad
permissions:blocks - jobs that could use OIDC but still rely on long-lived cloud secrets
- self-hosted runner exposure
- unsafe cache or artifact reuse
Prefer findings that point to a narrower trust shape:
permissions:
contents: read
id-token: write # only when the job actually uses OIDC federation
Flag workflows that hand out repository write or cloud credentials broadly when the job only needs read access or short-lived federated credentials.
3-G. Diff-Driven Filename Injection
Workflows that collect changed files and feed them into shell commands can become exploitable when attacker-controlled filenames are interpolated into command strings.
Check for diff-driven file handling first:
$workflowFiles = git --no-pager ls-files --cached --others --exclude-standard |
Select-String "^(\\.github/(workflows|actions)/.+|scripts/.+|.+\\.sh)$" |
ForEach-Object { $_.Line }
if ($workflowFiles) {
Select-String -Path $workflowFiles -Pattern "git diff --name-only|git diff-tree|GITHUB_OUTPUT|xargs|for file in|while read"
}
Prefer these patterns:
- NUL-delimited parsing for changed files:
git diff --name-only -z ... | while IFS= read -r -d '' file; do ...; done - Data-only workflow outputs written to
$GITHUB_OUTPUTor environment files, rather than constructing one shell command that embeds filenames directly. When a filename must cross a workflow boundary, encode it safely first (for example JSON, Base64, or another structured representation that preserves control characters) - Array-based execution in shell steps:
CMD=("tool" "$file"); "${CMD[@]}"
Do not trust:
git diff --name-onlyoutput pasted directly intorun:command strings- PR titles, branch names, or filenames concatenated into shell code
- raw filename writes to
$GITHUB_OUTPUTor$GITHUB_ENVwithout control-character-safe encoding xargsorforloops that split on whitespace when filenames may contain special characters
3-H. Hardening Follow-Through
After you find an exploitable pattern, check whether the workflow keeps its protective controls current:
- third-party actions pinned by tag instead of full commit SHA
- no automation for reviewing stale GitHub Actions pins
- organization-wide hardening intent documented, but individual workflows still inheriting broad defaults
A minimal update workflow can look like:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Treat missing pin maintenance as a hardening gap, not just a style issue.
4. Validate each finding before reporting
Every HIGH-confidence report should include:
- Entry point - how the external attacker triggers it
- Payload - what the attacker controls
- Execution mechanism - where the payload becomes code or a privileged action
- Impact - what the attacker gains
- PoC sketch - a concise attack path
If any link in that chain is weak, downgrade to MEDIUM or drop the finding.
5. Report only what survives the threat model
Use this structure:
## GHA Security Review
### HIGH
- **Pwn request** in `.github/workflows/release.yml`
- Entry point: fork PR triggers `pull_request_target`
- Payload: attacker modifies local action in PR branch
- Execution: workflow checks out PR head and runs local action
- Impact: repository write token theft
- PoC sketch: ...
### MEDIUM
- **Expression injection** in `.github/workflows/comment.yml`
- Needs verification: attacker-controlled comment body appears in `run:`
### No finding
- Workflow uses `pull_request` only and actions are pinned to full SHA
Confidence Rules
| Confidence | Meaning | Action |
|---|---|---|
| HIGH | Full attack path confirmed | Report with all five elements |
| MEDIUM | Meaningful path but one link still needs proof | Report as needs verification |
| LOW | Theoretical, mitigated, or outside the threat model | Do not report |
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It only runs in CI, not production." | CI often holds the credentials that production trusts. |
"The workflow uses pull_request_target, but that's normal." |
It is only safe when fork-controlled code never becomes executable. |
| "Expressions are everywhere in Actions YAML." | Expressions are dangerous specifically when attacker-controlled data reaches run: shell context. |
Red Flags
pull_request_targetplus fork checkout- attacker-controlled
${{ }}insiderun: - issue comment commands with no authorization check
- long-lived secrets reachable from untrusted code paths
- third-party actions pinned by tag instead of full SHA
- PR-controlled config files used as workflow instructions
- diff-derived filenames interpolated into shell commands without NUL-safe parsing or array passing
Verification
- The workflow trigger and trust boundary were confirmed from actual YAML
- Every finding fits the "external attacker without write access" model
- HIGH findings include entry point, payload, execution mechanism, impact, and PoC sketch
- Safe patterns were filtered out instead of over-reported
See Also
agent-owasp-check- audit broader agent trust-boundary and tool-governance risksagent-supply-chain- verify integrity and pinning for build or plugin artifactspr-security-review- review application-code diffs for classic security issues