GHA Security Review
SkillSecurityUse 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the GHA Security Review skill
What this skill tells your AI
The instructions your AI receives, as published by drvoss/everything-copilot-cli in skills/security/gha-security-review/SKILL.md and read by ahel’s 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
Signals
- GitHub stars
- 46
- Forks
- 11
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
gha-security-review- Source
- github.com/drvoss/everything-copilot-cli