Rules / GL046
GL046 Cache poisoning across the protected-branch boundary
Severity: warn; error for cache: unprotect: true on deploy-like or merge request jobs
OWASP: CICD-SEC-3 – Dependency Chain Abuse
CWE: CWE-494 – Download of Code Without Integrity Check
Risk
A cache: block whose key: is derived from a user-controlled variable allows an attacker to poison the cache for other pipelines. By creating a branch whose name is crafted to match the cache key of a protected branch or release pipeline, an attacker can pre-fill the cache with malicious build artefacts (e.g. tampered node_modules/, compiled binaries). A subsequent legitimate build will restore and execute those artefacts without integrity verification.
CI_COMMIT_REF_SLUG is a URL-safe normalisation of CI_COMMIT_REF_NAME and carries the same risk — an attacker can craft a branch name whose slug matches the target key.
What is flagged
GL046 covers two ways an unprotected branch can fill a cache that a protected pipeline restores.
Cache key from a user-controlled variable
Any cache: key: value (at job level or under default:) that contains a user-controlled predefined variable, and any cache: key: prefix: field that contains such a variable.
Flagged variables:
| Variable | Controlled by |
|---|---|
$CI_COMMIT_REF_NAME |
Pusher (branch or tag name) |
$CI_COMMIT_REF_SLUG |
Pusher (normalised branch/tag name) |
$CI_COMMIT_BRANCH |
Pusher (branch name) |
$CI_COMMIT_TAG |
Pusher (tag name) |
$CI_COMMIT_TITLE / $CI_COMMIT_MESSAGE |
Commit author |
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
MR author |
$CI_MERGE_REQUEST_TITLE |
MR author |
$CI_PIPELINE_NAME |
API caller |
$CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME |
External pull request author |
$CI_CONFIG_REF_URI |
Pusher (ends in the branch name) |
$CI_RELEASE_DESCRIPTION |
Release author |
$AI_FLOW_INPUT / $AI_FLOW_CONTEXT |
Author of the comment or merge request that triggered a GitLab Duo flow |
cache: unprotect: true on a job that writes the cache
GitLab keeps caches of protected and unprotected branches apart by default. cache:unprotect: true removes that separation — in GitLab's words, "users without access to protected branches can read and write to cache keys used by protected branches". Anyone who can push a branch can then plant artefacts that a protected or release job restores.
An entry is flagged when it sets unprotect: true and can write the cache: policy: pull-push (the default), policy: push, or a policy taken from a variable, which cannot be resolved statically. policy: pull only reads and is not flagged. Severity is error for deploy-like jobs (name, stage, or environment:) and for jobs that run on merge request pipelines, warn otherwise, including under default:.
build:
cache:
key: deps
unprotect: true # shared with unprotected branches, written by this job
paths:
- vendor/
Not flagged:
- Jobs that run only on the default branch or on protected refs — every
rules:entry that can run the job requires$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH,$CI_COMMIT_BRANCH == "main"/"master", or$CI_COMMIT_REF_PROTECTED == "true"(oronly:lists nothing butmain/master). Writing the shared cache from the protected side and reading it withpolicy: pullelsewhere is the documented use ofunprotect:. - Hidden template jobs (
.name). The job that extends them decides where they run, andextends:is not resolved.
Caches defined behind a YAML anchor (cache: *deps or <<: *deps) are checked like inline ones. A cache used by several jobs is reported once, at its definition, with the highest severity any of those jobs gives it.
Fix: drop unprotect:, or set policy: pull on jobs that share the cache with unprotected branches and build it only on protected ones.
Trigger examples
build:
cache:
key: $CI_COMMIT_REF_NAME # branch name controls the cache slot
paths:
- node_modules/
script:
- ./build.sh
test:
cache:
key:
files:
- package-lock.json
prefix: $CI_COMMIT_REF_NAME # prefix derived from branch name
paths:
- node_modules/
script:
- ./build.sh
Safe alternative
Key the cache on a file hash instead of a branch name. This makes the key deterministic and independent of attacker-controlled input:
build:
cache:
key:
files:
- package-lock.json # cache invalidates when lockfile changes
paths:
- node_modules/
script:
- ./build.sh
Notes
- Per-branch caches using
$CI_COMMIT_REF_NAMEare a common and documented GitLab CI pattern; this rule flags them because the security risk depends on the pipeline's trust model and whether fork-based MR pipelines have cache write access - A static key or a key based on only internal variables (e.g.
$CI_PROJECT_ID) is safe
Documentation for glsec 1.23.0. Source of this page · Rule implementation