Self-Hosted Docker Runner /cache Volume Allows Cache Poisoning Leading to RCE in Protected Branches
HackerOne report #3163155 by aphantom on 2025-05-26, assigned to @greg:
Report | Attachments | How To Reproduce
Report
Summary
Self-hosted Docker runners automatically mount a named volume at /cache upon registration. Because this volume is reused across all refs handled by the same runner, an attacker with Developer access can:
- Read artifacts produced by protected-branch pipelines.
- Poison the cache so that the next protected-branch job executes attacker-controlled code.
The impact is similar to CVE-2022-1423 but in a self-hosted runner context, leading to arbitrary code execution or secret exfiltration in pipelines that should be restricted to Maintainers.
Root Cause
During gitlab-runner register --executor docker, the helper function askDocker() silently appends "/cache" to the volume list unless DisableCache is set:
func (s *RegisterCommand) askDocker() {
s.askBasicDocker("ruby:2.7")
for _, v := range s.Docker.Volumes {
if strings.HasSuffix(v, "/cache") {
return // already present
}
}
if !s.Docker.DisableCache {
s.Docker.Volumes = append(s.Docker.Volumes, "/cache")
}
}
/cache is thus present even when the project’s .gitlab-ci.yml contains no cache. The volume’s lifecycle is bound to the runner slot (e.g., runner--project--concurrent-0-cache-...), not to the branch or cache-key, so protected and unprotected pipelines share the same physical directory.
Steps to reproduce
- Start a self-hosted Docker runner
docker volume create gitlab-runner-config
docker run -d --name gitlab-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitlab-runner-config:/etc/gitlab-runner \
gitlab/gitlab-runner:latest
- Register it to your project:
gitlab-runner register --url https://gitlab.com --token <REDACTED>
After registration /etc/gitlab-runner/config.toml contains volumes = ["/cache"] — added automatically.
- Run pipeline on an unprotected branch (attacker developer):
stages:
- test
test:
stage: test
image: python:3.12-slim
script:
- echo "malicious developer was here!!" > /cache/openme.txt
- As the
Owneruser, run pipeline on the protected main branch (victim):
stages:
- test
test:
stage: test
image: python:3.13-slim
script:
- ls -la /cache
- cat /cache/openme.txt
The protected-branch job mounts the same /cache volume, proving cross-branch access and demonstrating cache poisoning potential:
What is the current bug behavior?
Self-hosted Docker runners always mount the same named volume at /cache for every job slot. This volume persists across pipelines, so files written by an unprotected branch are automatically visible—and executable—in subsequent protected-branch jobs and vice versa on the same runner.
What is the expected correct behavior?
Protected-branch pipelines should never consume data originating from unprotected branches. The runner should either isolate /cache volumes per protection level/ref, or skip mounting /cache unless a project explicitly defines a cache, ensuring cross-branch separation as documented.
Relevant logs and/or screenshots
Below is a video POC of the repro steps showcased above:
<Redacted>
Output of checks
Below is the GitLab Runner version details:
Version: 18.0.2
Git revision: 4d7093e1
Git branch: 18-0-stable
GO version: go1.23.6 X:cacheprog
Built: 2025-05-21T22:00:57Z
OS/Arch: linux/arm64
Environment Info
Below is the config.toml of the runner that was auto-filled upon registration
concurrent = 1
check_interval = 0
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "92ed40c98d55"
url = "https://gitlab.com"
id = 47610935
token = <REDACTED>
token_obtained_at = 2025-05-26T14:16:30Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "docker"
[runners.cache]
MaxUploadedArchiveSize = 0
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "ruby:2.7"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"] <-- auto-configured
shm_size = 0
network_mtu = 0
Impact
Similar impact as CVE-2022-1423, a developer on an unprotected branch can stash files in the mounted /cache volume; the same files are then mounted into protected-branch pipelines, run with maintainer-level permissions. This lets the attacker execute arbitrary code, steal secret variables (tokens, deploy keys, etc.), and read or modify build outputs that should be restricted to trusted branches.
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section:

