Introduction
In the templates/devto/devto-readme.yml workflow file, two GitHub Actions steps at lines 18 and 21 referenced third-party actions using mutable version tags — actions/checkout@v4 and actions/setup-node@v4. While these tags point to legitimate releases today, they represent a ticking time bomb: any compromise of the upstream action repository could silently redirect these tags to malicious code, executing it in your CI/CD environment with full repository access.
This isn't theoretical. In 2024, both the trivy-action and kics-github-action were compromised through exactly this mechanism — attackers repointed existing tags to inject credential-stealing code into thousands of downstream workflows.
The workflow in question sets up a Node.js 20 environment for what appears to be a Dev.to README generation pipeline. The checkout step grants access to repository contents, and the setup-node step configures the runtime. Both are high-privilege operations that, if compromised, could exfiltrate secrets, modify source code, or pivot into production environments.
The Vulnerability Explained
What Are Mutable Tags?
In Git, tags are simply pointers to commits. Unlike commit SHAs (which are cryptographic hashes of the content), tags can be deleted and recreated pointing to a different commit. GitHub Actions uses Git references to resolve which version of an action to run.
When your workflow contains:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
GitHub resolves v4 to whatever commit that tag currently points to at runtime. If an attacker gains write access to the actions/checkout repository (or any third-party action you use), they can:
- Force-push the
v4tag to a new commit containing malicious code - Every workflow using
@v4immediately starts executing the attacker's code - No notification is sent to downstream consumers
The Attack Scenario for This Workflow
Consider this specific attack against templates/devto/devto-readme.yml:
- An attacker compromises the
actions/setup-noderepository (or a less-maintained action in your workflow) - They repoint the
v4tag to a commit that, in addition to setting up Node.js, also:
- Reads all repository secrets (${{ secrets.* }})
- Exfiltrates them to an attacker-controlled server
- Injects a backdoor into the generated README content - Since this is a private Node.js application, the attacker now has access to any deployment credentials, API keys, or tokens stored as GitHub secrets
The actions/checkout step is particularly dangerous because it runs first and has access to GITHUB_TOKEN, which by default can push code back to the repository.
Why Version Tags Provide Zero Security Guarantees
A version tag like @v4 provides:
- ❌ No integrity verification
- ❌ No immutability guarantee
- ❌ No audit trail of changes
- ❌ No protection against upstream compromise
A commit SHA like @11bd71901bbe5b1630ceea73d27597364c9af683 provides:
- ✅ Cryptographic integrity (SHA-1 hash of exact content)
- ✅ Immutability (cannot be changed without changing the hash)
- ✅ Deterministic builds (same code runs every time)
- ✅ Auditability (you can verify exactly what code will execute)
The Fix
The fix pins both action references to their exact commit SHAs while preserving version comments for human readability:
Before (Vulnerable)
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
After (Hardened)
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "20"
What Changed and Why
| Action | Before | After (SHA) | Version |
|---|---|---|---|
actions/checkout |
@v4 |
@11bd71901bbe5b1630ceea73d27597364c9af683 |
v4.2.2 |
actions/setup-node |
@v4 |
@49933ea5288caeca8642d1e84afbd3f7d6820020 |
v4.4.0 |
Key aspects of this fix:
-
Full 40-character SHA: The commit hash is immutable — even if the
v4tag is repointed, this workflow will continue running the verified code at the pinned commit. -
Version comments (
# v4.2.2,# v4.4.0): These trailing comments serve as documentation, making it easy for maintainers to know which version the SHA corresponds to when it's time to update. -
Specific patch versions: Rather than pinning to the vague
v4major version, the fix documents the exact minor/patch version (v4.2.2 and v4.4.0), providing better visibility into what's actually running. -
Behavioral preservation: The workflow still checks out the repository and sets up Node.js 20 — the only change is how GitHub resolves which code to run for each action.
Prevention & Best Practices
1. Always Pin to Commit SHAs
Every uses: directive in your GitHub Actions workflows should reference a full commit SHA:
# ❌ Vulnerable - mutable tag
uses: actions/checkout@v4
# ❌ Still vulnerable - minor version tags are also mutable
uses: actions/checkout@v4.2.2
# ✅ Secure - immutable commit SHA with version comment
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2. Automate SHA Updates
Use Dependabot or Renovate to automatically propose PR updates when pinned actions release new versions:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
3. Use GitHub's Verification Features
- Enable Artifact Attestations for actions you publish
- Review the Security tab of any action before using it
- Prefer actions from verified creators (blue checkmark)
4. Implement Least Privilege
- Set
permissions:explicitly in your workflow to limitGITHUB_TOKENscope - Use
readpermissions where possible instead of defaultwrite
5. Scan Workflows with Static Analysis
Use Semgrep, StepSecurity's harden-runner, or actionlint to catch mutable references:
semgrep --config "p/github-actions" .github/workflows/
Key Takeaways
- The
actions/checkout@v4reference intemplates/devto/devto-readme.yml:18was vulnerable to tag-repointing attacks — an attacker compromising the upstream action could silently execute arbitrary code in this workflow. - Pinning to
@11bd71901bbe5b1630ceea73d27597364c9af683makes the reference cryptographically immutable — no upstream change can alter what code runs without a deliberate PR to update the SHA. - Both
actions/checkoutandactions/setup-nodeneeded pinning — a supply chain is only as strong as its weakest link, and either action could serve as an entry point. - Version comments (
# v4.2.2) are essential for maintainability — without them, developers can't easily determine if their pinned SHA is outdated or what version it corresponds to. - Real-world compromises (trivy-action, kics-github-action) prove this isn't theoretical — automated attackers actively target the mutable-tag pattern in CI/CD pipelines.
How Orbis AppSec Detected This
- Source: The
uses:directives intemplates/devto/devto-readme.ymlat lines 18 and 21, which resolve external action code at runtime based on mutable Git references. - Sink: GitHub Actions runner execution engine, which downloads and runs whatever code the tag reference resolves to at the time of workflow execution.
- Missing control: No commit SHA pinning was in place — the workflow relied entirely on mutable tag names (
v4) that can be silently repointed by upstream repository owners or attackers. - CWE: CWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
- Fix: Replaced mutable tag references
@v4with full 40-character commit SHAs (@11bd71901bbe5b1630ceea73d27597364c9af683for checkout,@49933ea5288caeca8642d1e84afbd3f7d6820020for setup-node) while adding version comments for maintainability.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Supply-chain attacks against CI/CD pipelines are among the fastest-growing threat vectors in software security. The fix applied to templates/devto/devto-readme.yml is simple — replacing two mutable tag references with pinned commit SHAs — but it eliminates an entire class of attack. Every GitHub Actions workflow in your repository should follow this pattern: pin to SHAs, comment the version, and use automated tools to keep those pins current.
The cost of this hardening is minimal (slightly less readable uses: lines), but the security benefit is substantial: cryptographic assurance that the code running in your CI/CD pipeline is exactly what you reviewed and approved.