| name | gha-security-review |
| description | Use when reviewing GitHub Actions workflows for exploitable vulnerabilities — finds pwn-request patterns, expression injection, credential escalation, config poisoning, and supply chain risks, and reports only HIGH and MEDIUM confidence findings with concrete attack paths.
|
| metadata | {"category":"security","agent_type":"general-purpose","origin":"adapted from sickn33/antigravity-awesome-skills gha-security-review (MIT)"} |
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/*.yml or .github/workflows/*.yaml changes 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_dispatch manually
- 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/*.yml or .github/workflows/*.yaml
action.yml or action.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_target
pull_request
issue_comment
workflow_call
push
schedule
workflow_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
${{ }} in if: conditions
${{ }} in with: inputs
${{ secrets.* }} by itself
3-C. Unauthorized Command Execution
Review issue_comment workflows that parse slash commands or bot commands.
Check:
- whether
author_association is 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.md
CLAUDE.md
.cursorrules
Makefile
- 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
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_OUTPUT or 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-only output pasted directly into run: command strings
- PR titles, branch names, or filenames concatenated into shell code
- raw filename writes to
$GITHUB_OUTPUT or $GITHUB_ENV without control-character-safe encoding
xargs or for loops 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_target plus fork checkout
- attacker-controlled
${{ }} inside run:
- 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
See Also