READ-ONLY PACKAGE PREVIEW

getsentry-gha-security-review/references/permissions-and-secrets.md

Version d18b7aa8ba87.bb1 · Apache-2.0. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.

← Return to resource and package checksum

Permissions and Secrets

Overview

GitHub Actions workflows have access to GITHUB_TOKEN (automatic) and repository secrets (configured). Overly broad permissions amplify the impact of any vulnerability. Proper permission scoping is the single most effective mitigation for GitHub Actions attacks.


GITHUB_TOKEN Permissions

Default Permissions

GitHub offers two default permission modes (configured in repo Settings > Actions > General):

Mode Default Permissions
Read and write (legacy default) contents: write, packages: write, etc.
Read-only (recommended) contents: read only

Explicit Permission Scoping

# VULNERABLE: No explicit permissions — inherits repo default
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "What permissions does GITHUB_TOKEN have?"

# SAFE: Explicit minimal permissions at workflow level
on: pull_request
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "GITHUB_TOKEN can only read"

Permission Scopes

Permission Read Write Use Cases
contents Clone, read files Push commits, create releases Most workflows need read
pull-requests View PRs Comment, approve, merge PR review bots
issues View issues Create, comment, close Issue management
packages Pull packages Push packages Package publishing
deployments View deployments Create deployments CD workflows
id-token — Request OIDC token Cloud authentication
actions View workflow runs Cancel/rerun workflows Workflow management
security-events View alerts Upload SARIF, dismiss alerts Security scanning
statuses View statuses Create commit statuses CI status reporting

Dangerous Permission Patterns

# DANGEROUS: write-all grants everything
permissions: write-all

# DANGEROUS: Broad permissions on untrusted trigger
on: pull_request_target
permissions:
  contents: write
  pull-requests: write
  packages: write

# SUSPICIOUS: id-token on workflows that don't deploy
on: pull_request
permissions:
  id-token: write  # Why does a PR build need OIDC?

Secret Management

Secret Exposure Vectors

Via Expression Injection
# VULNERABLE: Secret value ends up in shell where injection can capture it
env:
  API_KEY: ${{ secrets.API_KEY }}
steps:
  - run: echo "Processing ${{ github.event.pull_request.title }}"
    # Expression injection here can access $API_KEY from environment
Via Workflow Logs
# VULNERABLE: Derived values from secrets may not be masked
- run: |
    ENCODED=$(echo "${{ secrets.API_KEY }}" | base64)
    echo "Encoded key: $ENCODED"  # GitHub only masks the original secret value

GitHub Actions automatically masks secret values in logs, but derived values (base64-encoded, truncated, transformed) are NOT masked.

Via Artifacts
# VULNERABLE: Secrets written to files that become artifacts
- run: |
    echo "${{ secrets.DEPLOY_KEY }}" > deploy_key.pem
    # If this file ends up in an artifact, the secret is exposed
- uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: .  # Includes deploy_key.pem!
Via Pull Request Target
# VULNERABLE: Fork code can access org secrets
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: ./build.sh  # Fork code can read all secrets via ${{ secrets.* }}

Important: Secrets are available to pull_request_target workflows. Combined with fork checkout, this exposes every secret to attacker code.


Detection Patterns

# Find workflows without explicit permissions
grep -L "permissions:" .github/workflows/*.yml

# Find workflows with write-all
grep -rn "write-all" .github/workflows/

# Find workflows with broad write permissions
grep -rn "write$" .github/workflows/ | grep -v "#"

# Find secret usage
grep -rn "secrets\." .github/workflows/

# Find secrets in pull_request_target workflows
for f in .github/workflows/*.yml; do
  if grep -q "pull_request_target" "$f" && grep -q "secrets\." "$f"; then
    echo "ALERT: $f uses secrets with pull_request_target"
  fi
done

# Find workflows that write secrets to files
grep -A2 "secrets\." .github/workflows/*.yml | grep -E ">\s|>>|tee "

# Check for environment-level secrets
grep -rn "environment:" .github/workflows/

OIDC Subject Claim Misconfiguration

GitHub OIDC tokens include a sub (subject) claim that cloud providers use to authorize access. Misconfigured subject claims can allow unauthorized workflows to assume cloud roles.

# VULNERABLE: Subject claim too broad — any branch can assume role
# AWS IAM trust policy
{
  "Condition": {
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:org/repo:*"  # Any ref!
    }
  }
}

# SAFE: Restrict to specific branch and environment
{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:org/repo:environment:production"
    }
  }
}

OIDC Best Practices

Claim Filter Risk Level When to Use
repo:org/repo:* High Never — any branch/PR can assume role
repo:org/repo:ref:refs/heads/main Medium Only main branch, but any workflow
repo:org/repo:environment:production Low Requires environment protection rules

The Fix: Minimal Permissions Pattern

Workflow-Level Defaults

# Set restrictive defaults at workflow level
permissions:
  contents: read

# Override per-job only when needed
jobs:
  lint:
    runs-on: ubuntu-latest
    # Inherits workflow permissions: contents: read
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Only this job needs OIDC
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4

Secret Scoping with Environments

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Secrets only available in this environment
    steps:
      - run: deploy --key ${{ secrets.DEPLOY_KEY }}

Environment protection rules add: - Required reviewers - Wait timers - Branch restrictions - Deployment protection rules


Severity Guidelines

Pattern Severity Rationale
write-all on pull_request_target Critical Maximum permissions + fork code execution
No permissions: block (inherits repo default) Medium May have write access depending on repo settings
contents: write on PR-triggered workflow High Allows pushing commits if combined with other vulns
Secrets in pull_request_target with fork checkout Critical All secrets exposed to attacker code
OIDC with wildcard subject claim High Any workflow can assume cloud role
Secrets written to files/artifacts High Persistent exposure beyond workflow run

References