Application & API Security • 3 min read
Running Security Scanners as a GitHub Action: Setup, Permissions and Pitfalls
A practical guide to running security scanning in GitHub Actions safely: least-privilege tokens, pinning actions to commit SHAs, fork pull request behavior, SARIF reporting and secret handling.
By CryptoAgile Labs Editorial Team • Published August 2026 • Last updated October 10, 2026
GitHub Actions is a convenient place to run scanners for secrets, vulnerable dependencies and insecure code on every pull request. But a security tool running in your pipeline is also code with access to your repository, so it needs the same care as production code. This guide covers a safe setup and the mistakes that cause real incidents.
A baseline workflow
name: security-scan
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write # only if uploading SARIF
concurrency:
group: scan-${{ github.ref }}
cancel-in-progress: true
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Run scanner
run: ./scripts/run-scanner.sh --format sarif --output results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarifKey choices: an explicit permissions block, a timeout, no persisted credentials, and a concurrency group that cancels superseded runs.
1. Use least-privilege tokens
Every run receives a GITHUB_TOKEN. Declare the permissions the job needs at the top of the workflow and nothing more. A scanner that only reads code needs contents: read. Add security-events: write only when uploading results, and pull-requests: write only if posting comments. Setting the repository default to read-only provides a safety net.
2. Pin third-party actions to a commit SHA
Tags such as @v4 can be moved by the action's maintainer, or by an attacker who compromises the maintainer's account. In March 2025 the widely used tj-actions/changed-files action was compromised and tags were repointed to malicious code that printed secrets into workflow logs. Workflows pinned to a full commit SHA were not affected.
- uses: actions/checkout@<full-40-character-commit-sha> # v4.x.xKeep a version comment next to the SHA and let a dependency bot propose updates. First-party actions from GitHub are lower risk but pinning is still good hygiene.
3. Understand pull request triggers
Behavior differs sharply by trigger:
| Trigger | Secrets available | Token permissions | Safe for untrusted forks? |
|---|---|---|---|
pull_request | Not for forks | Read-only for forks | Yes |
pull_request_target | Yes (base repo) | Can be write | No, if it checks out PR code |
pull_request_target runs in the context of the base repository with access to secrets. If it checks out and executes code from a fork's pull request, the contributor's code runs with your secrets. Use pull_request for scanning untrusted code. If you need to comment on fork PRs, use a two-stage design: a no-secret workflow produces artifacts, and a separate trusted workflow reads them as data only.
4. Handle secrets carefully
- Store credentials in repository or environment secrets, never in the workflow file.
- Do not print secrets or pass them on command lines that appear in logs. Masking is a best-effort feature, not a guarantee.
- Prefer short-lived credentials via OpenID Connect to your cloud provider over long-lived access keys stored as secrets.
- Scope environment secrets to protected branches or require reviewers for deployments.
5. Avoid script injection
Expressions like ${{ github.event.pull_request.title }} are substituted into shell scripts before they run. A malicious title can inject commands. Pass untrusted values through environment variables instead:
- name: Safe usage
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $PR_TITLE"6. Report findings where developers look
Upload results as SARIF so findings appear as annotations on the changed lines and in the repository's code scanning view. Developers act on feedback in the pull request far more than on a separate dashboard. See API security linting for how to tune which findings block a merge.
7. Manage cost and speed
- Cache scanner databases between runs.
- Scan changed files on pull requests and the full repository on a schedule.
- Set
timeout-minutesso a hung scanner cannot consume your quota.
Checklist
- Explicit minimal
permissionson every workflow. - Third-party actions pinned to commit SHAs.
pull_requestrather thanpull_request_targetfor untrusted code.- No secrets in logs; OIDC instead of static cloud keys where possible.
- Untrusted event data passed via environment variables.
- SARIF upload and a documented process for handling findings.
Sources and further reading
Related articles
This article is for educational purposes and is not professional security, legal or compliance advice. Spotted an error or an out-of-date detail? Email us a correction. See our editorial policy.