How to Prevent Poisoned GitHub Actions Dependencies

How to Prevent Poisoned GitHub Actions Dependencies


By ILYAS RUFAI | September 18, 2026 | #DevSecOps


Your workflow uses actions/checkout@v4. Today, that tag points to a vetted release. Tomorrow, a compromised maintainer or attacker moves the tag to malicious code. Your pipeline runs it with access to GITHUB_TOKEN.


Third-party Actions are dependencies. A floating tag behaves like an unpinned package dependency. If the reference changes, the workflow can execute different code without any change to your repository.


This article shows you how to prevent poisoned continuous integration (CI) dependencies by pinning Actions to full commit SHAs, validating those pins against release tags, restricting permitted actions, and automating reviewed updates with Dependabot.


Who this is for: Platform and DevSecOps engineers securing GitHub Actions workflows.


Prerequisites:


  • Repository admin access
  • Workflows using uses: org/action@ref syntax

The Quick Reference


Here's what you'll learn how to do here:


  • Pin every uses: reference to a full commit SHA, not a moving tag such as @v4.
  • Validate that each recorded SHA matches the release you reviewed before approving it.
  • Maintain an allowlist of permitted actions at the organization or repository level.
  • Enable Dependabot for GitHub Actions version bumps with review.
  • Prefer official or verified creators. Mirror critical actions internally if needed.
  • Verify pins in pull request checks before merge.

Table of Contents



Why Floating Tags Fail


A GitHub Actions reference like @v4, @main, or @latest is a mutable pointer. Whoever controls the source repository—or gains access to it—can move that pointer to any commit at any time. Your workflow does not change, but the code it executes does.


This is the same class of risk as an unpinned npm or pip dependency, with two aggravating factors specific to CI:


  1. Privileged execution. Workflow steps run with the permissions granted to the job, often including a GITHUB_TOKEN, repository secrets, and cloud credentials via OIDC.
  2. Supply chain reach. A poisoned action can exfiltrate secrets, tamper with build artifacts, or push malicious commits across every repository that consumes it.

  3. Common attack patterns include:


    • Tag retargeting. A maintainer account is compromised and an existing release tag is force-moved to a malicious commit.
    • Compromised maintainer. A trusted contributor merges a backdoor and publishes it under a new minor version.
    • Typosquatting. A workflow references a similarly named action (actions/checkout-2 instead of actions/checkout).
    • Transitive actions. A pinned action calls other, unpinned actions internally.

    The mitigation for all of these is the same: make the reference immutable and verify what it resolves to.


    Pin Actions to Commit SHAs


    A full 40-character commit SHA is cryptographically bound to the exact tree of files it represents. It cannot be retargeted without detection. Replacing a tag with a SHA prevents silent code substitution.


    Before:


    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
    

    After:


    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
    

    Best practices when pinning:


    • Always pin to the full SHA, not a shortened one. Abbreviated SHAs are ambiguous and can collide.
    • Keep the version in a trailing comment (for example, # v4.2.2) so reviewers can see intent at a glance.
    • Pin the actions you trust implicitly. This includes first-party, internal, and mirrored actions—the goal is immutability, not distrust.
    • Pin nested actions too. If an action invokes others internally, verify those depend on SHA-pinned references or mirror the action internally.

    You can automate the rewrite using community tooling such as pin-github-action, ratchet, or zizmor, which audit and replace mutable references across your workflow files.


    Validate the Commit SHAs


    A SHA is only trustworthy if it maps to the release you actually reviewed. After pinning, confirm each SHA resolves to the expected tag.


    Option 1: Manual verification


    git ls-remote https://github.com/actions/checkout refs/tags/v4.2.2
    

    The output returns the commit the tag currently points to. Compare it to the SHA in your workflow. If they diverge, the tag has moved and you should investigate before bumping.


    Option 2: Automated verification in CI


    Add a workflow step that fails if a pinned SHA no longer matches its referenced tag:


    - name: Verify pinned action SHAs
      run: |
        grep -rEo 'uses: [^@]+@[a-f0-9]{40}' .github/workflows/ | while read -r line; do
          # Extract action and SHA, resolve tag, and compare
          # Tooling such as zizmor or ratchet can perform this check
          echo "Verifying: $line"
        done
    

    Dedicated tools offer stronger coverage:


    • zizmor — static analysis for GitHub Actions, including unpinned reference detection.
    • ratchet — updates pinned SHAs and posts validation reports on pull requests.
    • Scorecard — OpenSSF's checker scores pinned dependency hygiene across repositories.

    Treat a failed pin validation as a blocking error. A moved tag is a security event, not a maintenance nuisance.


    Restrict Permitted Actions with an Allowlist


    Pinning is necessary but not sufficient. A compromised developer or an unreviewed pull request can still introduce an action you never intended to allow.


    GitHub supports restricting actions at both the enterprise and organization level via Actions permissions. Set the policy to Allow select actions and add the actions you actually use.


    Key configuration points:


    • Allow only specified actions and reusable workflows. Reject everything not explicitly listed.
    • Allow GitHub-authored actions and optionally verified marketplace creators to reduce the maintenance burden.
    • Require full commit SHAs via the policy option Require actions to be pinned to a full-length commit SHA. This enforces pinning at the platform level rather than relying on code review.
    • Block the wildcard *. Allowlisting everything defeats the purpose.
    • Scope to the organization so new repositories inherit the policy by default.

    For the highest-risk actions—those that handle credentials, publish artifacts, or deploy to production—consider mirroring them internally. Maintain a fork in your own organization, review upstream changes on your own schedule, and reference the internal copy. This removes your dependency on the upstream project's security posture and availability.


    Automate Reviewed Updates with Dependabot


    Pinning to SHAs creates a maintenance problem: pins go stale, and stale pins miss security fixes. Dependabot solves this by opening reviewed pull requests when a pinned action's upstream tag moves to a new release.


    Configure it in .github/dependabot.yml:


    version: 2
    updates:
      - package-ecosystem: "github-actions"
        directory: "/"
        schedule:
          interval: "weekly"
        open-pull-requests-limit: 10
        commit-message:
          prefix: "ci"
        groups:
          actions:
            patterns:
              - "actions/*"
    

    How this works in practice:


    1. A pinned action publishes a new release.
    2. Dependabot opens a pull request updating the SHA and the trailing version comment.
    3. CI runs your pin-validation checks and unit tests.
    4. A reviewer confirms the release notes and merges.

    5. Dependabot updates only to released tags, not to arbitrary commits on main. This gives you a concrete release to review each time. Combine it with the allowlist and SHA policy so that any action not on the list fails regardless of what Dependabot proposes.


      For defense in depth, use groups to batch routine updates into a single pull request, reducing review fatigue while keeping security-relevant bumps visible.


      Verify Pins in Pull Request Checks


      Prevention depends on enforcement at merge time. Add a required status check that fails any pull request introducing an unpinned, unvalidated, or unallowlisted action.


      An effective check verifies four properties:


      1. Every uses: reference is pinned to a full 40-character SHA.
      2. Every SHA resolves to a tag or release matching its comment.
      3. Every action is on the allowlist for the organization.
      4. Nested and reusable workflow calls are also pinned, including uses: at the workflow level for reusable workflows.

      5. Implementation options:


        • Run zizmor or ratchet in a dedicated workflow triggered on pull_request.
        • Add a lightweight script as an inline step, but prefer maintained tooling for coverage and updates.
        • Mark the job as a required check via branch protection so failures block merge.

        Example minimal setup:


        name: Workflow security check
        
        on:
          pull_request:
            paths:
              - '.github/workflows/**'
        
        permissions:
          contents: read
        
        jobs:
          zizmor:
            runs-on: ubuntu-latest
            steps:
              - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
                with:
                  persist-credentials: false
              - uses: zizmorcore/action@<full-sha> # v0.x.x
        

        Note the persist-credentials: false setting—this prevents the checkout step from leaving the GITHUB_TOKEN available to subsequent steps, shrinking the blast radius if a later action is compromised.


        Summary


        Poisoned GitHub Actions dependencies exploit mutable references and privileged CI execution. The defenses layer together:


        • Immutable references. Pin every uses: to a full commit SHA.
        • Verified pins. Confirm each SHA matches the release you reviewed.
        • Restricted surface. Apply an organization-wide allowlist and require SHA pinning at the platform level.
        • Automated, reviewed updates. Use Dependabot to keep pins current without trusting them blindly.
        • Merge-time enforcement. Block unpinned or unallowlisted actions in required pull request checks.

        None of these controls is sufficient on its own. Together, they turn a floating tag from a silent backdoor into a reviewable, auditable, and enforced dependency.

        via FreeCodeCamp

Related