Cache Poisoning in GitHub Actions¶
ID: actions/cache-poisoning/code-injection
Kind: path-problem
Security severity: 7.5
Severity: error
Precision: high
Tags:
- actions
- security
- external/cwe/cwe-349
- external/cwe/cwe-094
Query suites:
- actions-code-scanning.qls
- actions-security-extended.qls
- actions-security-and-quality.qls
Click to see the query in the CodeQL repository
Description¶
GitHub Actions cache poisoning is a technique that allows an attacker to inject malicious content into the Action’s cache from unprivileged workflow, potentially leading to code execution in privileged workflows.
An attacker with the ability to run code in the context of the default branch (e.g. through Code Injection or Execution of Untrusted Code) can exploit this to:
Steal the cache access token and URL.
Overflow the cache to trigger eviction of legitimate entries.
Poison cache entries with malicious payloads.
Achieve code execution in privileged workflows that restore the poisoned cache.
This allows lateral movement from low-privileged to high-privileged workflows within a repository.
Cache Structure¶
In GitHub Actions, cache scopes are primarily determined by the branch structure. Branches are considered the main security boundary for GitHub Actions caching. This means that cache entries are generally scoped to specific branches.
Access to Parent Branch Caches: Feature branches (or child branches) created off of a parent branch (like
main
ordev
) can access caches from the parent branch. For instance, a feature branch off ofmain
will be able to access the cache frommain
.Sibling Branches: Sibling branches, meaning branches that are created from the same parent but not from each other, do not share caches. For example, two branches created off of
main
will not be able to access each other’s caches directly.
Due to the above design, if something is cached in the context of the default branch (e.g., main
), it becomes accessible to any feature branch derived from main
.
Recommendations¶
Avoid using caching in workflows that handle sensitive operations like releases.
If caching must be used:
Validate restored cache contents before use.
Use short-lived, workflow-specific cache keys.
Clear caches regularly.
Implement strict isolation between untrusted and privileged workflow execution.
Never run untrusted code in the context of the default branch.
Sign the cache value cryptographically and verify the signature before usage.
Examples¶
Incorrect Usage¶
The following workflow is vulnerable to code injection in a non-privileged job but in the context of the default branch.
name: Vulnerable Workflow
on:
issue_comment:
types: [created]
jobs:
pr-comment:
permissions: {}
runs-on: ubuntu-latest
steps:
- run: |
echo ${{ github.event.comment.body }}
Correct Usage¶
The following workflow is not vulnerable to code injections even if it runs in the context of the default branch.
name: Secure Workflow
on:
issue_comment:
types: [created]
jobs:
pr-comment:
permissions: {}
runs-on: ubuntu-latest
steps:
- env:
BODY: ${{ github.event.comment.body }}
run: |
echo "$BODY"