The Product Security Playbook

How to Use GitHub Actions Safely

By Chad Butler ·

Review the action’s source code before use, pin it to a specific commit hash (not a version tag), and monitor for updates through pull requests. Never auto-update.

Why this matters: GitHub Actions run with access to your secrets, source code, and deployment pipelines. When an attacker compromises a popular action’s maintainer account, they can inject malicious code that exfiltrates secrets, backdoors your application, or pivots into your infrastructure. Version tags are mutable. An attacker can retag v1.2.3 to point at malicious code. Only commit hashes are immutable.

What you’ll get:

  • A repeatable process for vetting third-party actions before first use
  • Actions pinned to cryptographically-verified commits that can’t be altered upstream
  • A monitoring system that alerts you to updates without auto-applying them

Prerequisites:

  • GitHub repository with Actions enabled
  • Permission to modify workflow YAML files
  • (Optional) Static analysis or AI tools for code review

Common obstacles (name the dragons):

  • The convenience trap: Version tags (like @v1.2.3) are easier to read but can be remapped to malicious commits
  • Update fatigue: Teams avoid pinning because they fear falling behind on security patches
  • False sense of security: Assuming popular actions are safe because “someone would have noticed”

Step 1: How do I review a GitHub Action’s code before using it?

Direct answer: Navigate to the action’s source repository, read the code in the exact version you plan to use, and scan it for credential access, network calls, and filesystem operations.

Context & theory: Most GitHub Actions request broad permissions by default. They can access your secrets, modify code, and make network calls. A compromised action might look normal at first glance but contain obfuscated exfiltration code. The goal isn’t to audit every line; it’s to understand what the action does and verify it aligns with your risk tolerance.

Do this:

  1. Go to github.com/marketplace and search for the action (example: “zap baseline scan”)
  2. Click “View source code” to open the repository
  3. Review the main action file (usually action.yml and the entry point script)
  4. Look for red flags:
    • Unencrypted network calls to unknown domains
    • Access to GITHUB_TOKEN or other secrets beyond stated purpose
    • Obfuscated code or encoded payloads
    • Dependencies that don’t match the action’s stated function
  5. (Optional) Run static analysis or use an AI assistant to flag suspicious patterns
  6. Document your findings and approval decision

Example:

  • Scenario: You need to add zaproxy/action-baseline@v0.14.0 for dynamic scanning
  • Action taken: Reviewed action.yml and entry script, confirmed it only posts results to GitHub and doesn’t exfiltrate secrets
  • Result: Approved for use with network-restricted runner

Key takeaway: Popular doesn’t mean safe. Vet every action like you’re granting it root access to your systems.


Step 2: How do I pin a GitHub Action to an immutable version without trusting mutable tags?

Direct answer: Replace the version tag with the full SHA-256 commit hash from the exact commit you reviewed, ensuring no one can silently swap the code.

Context & theory: GitHub Actions support three reference formats: branches (@main), tags (@v1.2.3), and commit hashes (@a1b2c3d...). Branches and tags are mutable. A compromised maintainer can reassign them. Only the full 40-character commit hash is cryptographically immutable. Pinning to a hash means you run exactly the code you vetted, forever, until you explicitly choose to update.

Do this:

  1. In the action’s repository, click “Tags” to see all versions
  2. Click the version you reviewed (e.g., v0.14.0)
  3. Note the commit it points to and click that commit
  4. Click the clipboard icon next to the commit hash to copy the full 40-character SHA
  5. In your workflow YAML, replace uses: owner/action@v0.14.0 with uses: owner/action@a1b2c3d4e5f6... (full hash)
  6. Add a comment above the action documenting which version the hash corresponds to:
    # zaproxy/action-baseline v0.14.0
    - uses: zaproxy/action-baseline@8a543f67e1d9c2b3a4f5e6d7c8b9a0f1e2d3c4b5
  7. Commit with message: “Pin zaproxy/action-baseline to v0.14.0 commit hash”

Example:

  • Before: uses: zaproxy/action-baseline@v0.14.0 (mutable tag, can be reassigned)
  • After: uses: zaproxy/action-baseline@8a543f67e1d9c2b3a4f5e6d7c8b9a0f1e2d3c4b5 (immutable hash)
  • Notes: If the action’s account is compromised and v0.14.0 is re-tagged, your pipeline still runs the original, vetted code

Key takeaway: Version tags are bookmarks that can be moved; commit hashes are permanent fingerprints.


Step 3: How do I keep these actions updated without reintroducing risk?

Direct answer: Use Dependabot or Renovate to raise pull requests when new versions are published, then re-review and re-pin only after approval.

Context & theory: Pinning to a hash solves the trust problem but creates an update problem. You need patches and features from newer versions, but you can’t auto-apply them without re-vetting. The solution is a pull-request-driven workflow: tools detect updates, you review the diff between your pinned version and the new one, and you merge only after verifying safety.

Do this:

  1. Enable Dependabot for GitHub Actions in your repository settings (Settings > Code security > Dependabot > Enable for Actions)
  2. Configure .github/dependabot.yml:
    version: 2
    updates:
      - package-ecosystem: "github-actions"
        directory: "/"
        schedule:
          interval: "weekly"
        open-pull-requests-limit: 5
  3. When Dependabot opens a PR, review the “Commits” tab to see the diff between your pinned hash and the proposed hash
  4. Re-apply Step 1’s review process to the changes
  5. If safe, approve and merge; Dependabot will update the hash automatically
  6. (Optional) Set up CODEOWNERS to require security team review for workflow changes

Example:

  • Instrumentation: Dependabot checks weekly, opens PR when zaproxy/action-baseline releases v0.15.0
  • Signal: PR shows only a dependency bump (safe), no new network calls or secret access
  • Maintenance: Security team reviews within 48 hours, merges, and documents approval

Key takeaway: Automate discovery, not approval. Let tools find updates, but gate them behind human review.


Summary: What you did and what you now have

In 3 lines:

  • You answered: How do I avoid breaches from compromised third-party GitHub Actions?
  • You implemented: Code review for new actions, commit-hash pinning, and PR-gated updates
  • You achieved: Immutable action versions, visibility into changes, and control over your supply chain

Results you can expect:

  • Zero risk of silent action compromise via re-tagged versions
  • Full audit trail of what code runs in your pipelines and when you approved it
  • 2-5 minutes per action to review updates vs. hours recovering from a breach

Key takeaways (copy/paste ready)

  • Step 1: Vet every action’s code like you’re granting root access to your infrastructure
  • Step 2: Pin to full commit hashes. Tags can be reassigned, hashes are cryptographically permanent
  • Step 3: Automate discovery of updates, gate approval behind security review and re-vetting

Checklist (one glance “done” list)

  • Reviewed action source code and documented approval decision
  • Replaced version tag with full 40-character commit hash
  • Added comment mapping hash to human-readable version
  • Enabled Dependabot or Renovate for GitHub Actions
  • Configured review requirement for workflow file changes

Sources & further reading


Ready to build secure CI/CD pipelines?

This is one of 12 critical workflows we cover in DevSecOps Pro … My course for AppSec engineers who want to ship secure code faster without slowing down development.

You’ll learn:

  • How to design secure pipelines that developers actually use
  • Secrets management that scales beyond .env files
  • Supply chain controls for every ecosystem (npm, PyPI, containers, Actions)
  • Threat modeling for CI/CD infrastructure

Learn more and enroll in DevSecOps Pro >


Frequently asked

Questions

What if I'm already using dozens of actions with version tags?

Audit them in priority order: start with actions that access secrets, deploy to production, or modify code. Use git log -p .github/workflows/ to see when each action was added, and review from newest to oldest. Budget 5-10 minutes per action for review and re-pinning.

Can I use short commit hashes (7 characters) instead of full hashes?

No. Short hashes are vulnerable to collision attacks. GitHub Actions require the full 40-character SHA-256 hash for security. The extra verbosity is the cost of immutability.

How do I know if a new version is worth updating to?

Compare commits between your pinned hash and the proposed hash using the action's repository compare view: https://github.com/owner/action/compare/OLD_HASH...NEW_HASH. If the diff includes only dependency patches or bug fixes (no new permissions or network calls), it's usually safe. If it introduces new functionality, re-apply your full review process.

What if Dependabot doesn't support my setup?

Renovate is a more flexible alternative that supports GitHub Actions, GitLab CI, and many other ecosystems. You can also write a simple script to check for new tags and open issues manually.

Get the next one

Subscribe to the Playbook

You've got the playbook

Now put it to work with us.

Whether you need a pipeline built, a team trained, or a decision pressure-tested, we help product security leaders turn strategy into shipped, secure software.