github-actions-security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited github-actions-security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
GitHub Actions runs code with access to your secrets, your code, and increasingly your cloud accounts. Most teams ship workflows with the defaults, which are convenient but expose more than necessary. This skill is the working baseline for production-grade Actions usage.
uses: someone/some-action@v1).github/workflows/uses: someone/action@v1 looks safe. It's not. Tags and branches can be moved at any time. If the action's maintainer is compromised, every workflow using @v1 runs the new attacker code on next CI.
Pin to a commit SHA. Comment the version for readability:
# Bad
- uses: actions/checkout@v4
# Good
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1For your own org / first-party actions, tag pinning is acceptable because you control the tag. For external actions: SHA pin.
dependabot can update SHA pins automatically — turn it on for .github/workflows/:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule: { interval: weekly }GITHUB_TOKEN permissionsGITHUB_TOKEN is created per workflow run. Its default permissions are broad — write to repo contents, issues, PRs, packages. Most workflows need read.
Set the floor at the workflow level, raise per-job only when needed:
name: CI
# Workflow-level default — read-only
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
# inherits read-only
steps:
- uses: actions/checkout@<sha>
- run: npm test
release:
runs-on: ubuntu-latest
needs: test
# Raise only for this job
permissions:
contents: write # for creating tags/releases
packages: write # for publishing
steps:
- uses: actions/checkout@<sha>
- run: npm publishOr set repo-wide default-restricted in Settings → Actions → General → Workflow permissions → "Read repository contents and packages permissions". Then individual workflows opt in to writes explicitly.
Storing AWS_ACCESS_KEY_ID / GCP_SA_KEY / CLOUDFLARE_API_TOKEN as repository secrets means: if a workflow logs them, leaks them, or a malicious action exfiltrates them, the credential is good until you notice and rotate. OIDC eliminates that — GitHub mints a short-lived token per workflow run that your cloud provider trusts based on configurable claims.
AWS example:
permissions:
id-token: write # required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: aws-actions/configure-aws-credentials@<sha>
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy-myrepo
aws-region: eu-central-1
- run: aws s3 sync ./dist s3://my-bucket/The IAM role's trust policy restricts which repo + branch can assume it:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main" }
}
}GCP, Cloudflare, Azure, HashiCorp Vault, and most modern providers support similar federation. Use it.
pull_request_target is dangerous by defaultpull_request_target runs in the context of the base repo with base-repo secrets — but the workflow file and code can be from the PR. An attacker opens a PR that modifies a workflow to print all secrets, GitHub helpfully runs it with access to those secrets.
# Bad — runs PR-author code with repo secrets
on: pull_request_target
jobs:
test:
steps:
- uses: actions/checkout@<sha>
with: { ref: ${{ github.event.pull_request.head.sha }} }
- run: npm test # PR-author code executes with full secret accessPatterns:
pull_request_target (e.g. for labeling, comment automation), do not check out PR code. Only operate on metadata.GitHub Action expressions interpolate strings into shell scripts. Untrusted input that contains ; rm -rf / becomes that, executed.
# Bad — issue title is attacker-controlled
- run: echo "New issue: ${{ github.event.issue.title }}"
# Good — pass through env
- env:
TITLE: ${{ github.event.issue.title }}
run: echo "New issue: $TITLE"Anything user-controllable — issue title/body, PR title/body, commit messages, branch names, fork names — must go through env: and be referenced as a shell variable. Same for head_ref, head_repository_name.
Before you uses: someone/some-action@<sha>:
composite and node actions can call other actions; check transitive deps.A reasonable rule: third-party actions for non-trivial work need a 5-minute review before adoption. Pinning by SHA is necessary but not sufficient.
production), with optional reviewer approval before deployment proceeds.echo "$MY_SECRET" will be redacted in logs, but echo "$MY_SECRET" | base64 will not.read-only GITHUB_TOKEN since 2021. Don't grant write back unless you intentionally support auto-merge.For deploy-to-prod workflows, GitHub Environments provide a real release gate:
jobs:
deploy-prod:
environment:
name: production
url: https://example.com
runs-on: ubuntu-latest
steps:
- run: ./deploy.shIn Settings → Environments → production:
main (or a specific branch list) can deploy to this environmentIf you suspect a workflow leaked a secret:
echo, printenv, env, cat ~/.env, xxd, base64 near a secret is a smell.#!/usr/bin/env bash
# Run inside a repo
echo "=== Unpinned third-party actions (uses: org/action@tag) ==="
grep -rE 'uses:\s+[^a-z]*([a-z0-9_-]+)/([a-z0-9_-]+)@(v[0-9]|main|master)' .github/workflows/ \
| grep -vE 'uses:\s+(actions|github)/' || echo "none"
echo "=== Workflows without explicit permissions ==="
for f in .github/workflows/*.{yml,yaml}; do
[ -f "$f" ] || continue
grep -q '^permissions:' "$f" || echo "$f"
done
echo "=== Workflows using pull_request_target ==="
grep -lE '^\s*pull_request_target' .github/workflows/*.{yml,yaml} 2>/dev/null || echo "none"
echo "=== Workflows accessing secrets ==="
grep -lE 'secrets\.[A-Z_]+' .github/workflows/*.{yml,yaml} 2>/dev/null
echo "=== Workflows that might echo env ==="
grep -rE 'echo.*\$|printenv|env\s*$' .github/workflows/ 2>/dev/null | head -10permissions: block set, read-only by defaultpull_request_target (or, if needed, doesn't check out PR code)${{ }} interpolation of user-controlled strings into shell — uses env: insteadgithub-actionspull_request_target + PR-code checkout for general CI~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.