Repository navigation
Description
Actions cache-poisoning query ignores workflow/job cache-mode
Description of the false positive
actions/cache-poisoning/poisonable-step reports the same finding whether a job
has read-only or writable cache authority. GitHub documents cache-mode: read
as backend-enforced through scoped cache tokens, preventing cache saves.
I reproduced this with CodeQL CLI 2.27.4, codeql/actions-queries 0.6.39,
and codeql/actions-all 0.7.1 from the checksum-verified Linux bundle.
The query pack's build SHA is
1be292a1e54696ca543b4ad0f9b4537b9687caa7.
Code samples
This is synthetic source. It was extracted and analyzed locally; no workflow,
checkout, or npm command was executed.
name: Cache-mode reproduction
on:
workflow_dispatch:
inputs:
source_sha:
description: Commit to inspect
required: true
type: string
permissions:
contents: read
cache-mode: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.source_sha }}
- run: npm ciFour separate source roots differed only in their cache policy:
| Variant | Workflow cache-mode | Job cache-mode | Expected findings | Observed findings |
|---|---|---|---|---|
| read | read | absent | 0 | 1 |
| default | absent | absent | 1 | 1 |
| job-read | absent | read | 0 | 1 |
| job-write | read | write | 1 | 1 |
The write-capable cases are positive controls, assuming the query's threat
model for manually supplied source. A fix must account for job overrides,
rather than suppress every workflow that contains a read-only declaration.
Each result reports:
Potential cache poisoning in the context of the default branch due to
privilege checkout of untrusted code from inputs.source_sha.
(workflow_dispatch).
Commands
Run these for each variant, using separate database and output paths:
codeql database create db-read \
--language=actions --build-mode=none --source-root=fixtures/read
codeql database analyze db-read \
'codeql/actions-queries@0.6.39:Security/CWE-349/CachePoisoningViaPoisonableStep.ql' \
--threads=2 --ram=4096 --format=sarif-latest --output=read.sarif
jq '[.runs[].results[]? |
select(.ruleId == "actions/cache-poisoning/poisonable-step")] | length' \
read.sarifRelevant model and platform contract
The cache-write predicate considers the triggering event, but does not inspect
explicit workflow/job cache policy:
codeql/actions/ql/lib/codeql/actions/security/CachePoisoningQuery.qll
Lines 45 to 64 in 1be292a
| private string defaultBranchCacheWriteEvent() { | |
| result = | |
| [ | |
| "push", "workflow_dispatch", "repository_dispatch", "delete", "registry_package", | |
| "page_build", "schedule" | |
| ] | |
| } | |
| private predicate eventHasDefaultBranchCacheWriteAccess(Event event) { | |
| runsOnDefaultBranch(event) and event.getName() = defaultBranchCacheWriteEvent() | |
| } | |
| /** | |
| * Holds if `job` can write to the cache scope of the default branch for `event`. | |
| * Reusable workflow jobs inherit their caller's trigger event. | |
| */ | |
| predicate hasDefaultBranchCacheWriteAccess(LocalJob job, Event event) { | |
| job.getATriggerEvent() = event and | |
| eventHasDefaultBranchCacheWriteAccess(event) | |
| } |
GitHub's documented policy:
Reusable-workflow caller limits/inheritance should also be covered by a model
fix, but those cases were not included in this minimal executed reproduction.
This report verifies static query behavior; it does not independently test
cache-token enforcement in the backend.
Authored by GitHub Copilot (GPT-6.1 Sol).
Activity
sam-robson commentedon Nov 4, 2026
Thanks for the report. Work is ongoing to add
cache-modesupport to the Actions cache-poisoning queries in #22613.