1SEAL-2026-003: path traversal in Tekton Pipelines git resolver — from tenant to cluster-wide secret access
summary
a path traversal vulnerability in the Tekton Pipelines git resolver allows a namespace-scoped
tenant to read arbitrary files from the resolver pod's filesystem, including its ServiceAccount
token. in the default RBAC configuration, this token grants get, list,
and watch on Secrets cluster-wide — escalating from a single namespace to
full cluster secret access.
the vulnerability exists because the pathInRepo parameter is passed directly to
os.ReadFile() without validation. the initial vendor patch was found to be
bypassable via symlinks committed inside attacker-controlled repositories; the final fix
includes both path validation and filepath.EvalSymlinks() containment.
severity
CRITICAL — CVSS 3.1 Base Score: 9.6
Vector: AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
affected versions
all releases of Tekton Pipelines from v1.0.0 through v1.10.0, including all patch releases (v1.3.1, v1.3.2, v1.9.1).
releases prior to v1.0.0 (e.g., v0.70.0 and earlier) are not affected —
they used the go-git library's in-memory filesystem (memfs) where path traversal
cannot escape the git worktree.
patched versions
| branch | patched release |
|---|---|
| v1.0.x | v1.0.1 |
| v1.3.x | v1.3.3 |
| v1.6.x | v1.6.1 |
| v1.9.x | v1.9.2 |
| v1.10.x | v1.10.2 |
root cause
the git resolver's getFileContent() function in
pkg/resolution/resolver/git/repository.go constructs a file path by joining the
repository clone directory with the user-supplied pathInRepo parameter:
fileContents, err := os.ReadFile(filepath.Join(repo.directory, path))
the pathInRepo parameter is accepted from the user without any validation. an
attacker can supply ../../../../etc/passwd or any other traversal sequence to
escape the cloned repository directory and read arbitrary files from the resolver pod's real
filesystem.
the vulnerability was introduced in commit 318006c4e3a5 ("fix: resolve Git
Anonymous Resolver excessive memory usage"), which switched the git resolver from the go-git
library — using an in-memory filesystem that cannot be escaped — to shelling out
to the git binary and reading files with os.ReadFile() from the
real filesystem.
a TODO comment in the source code acknowledged that validation was never
implemented:
// TODO(sbwsg): validate pathInRepo is valid relative pathInRepo
exploitation chain
the attack proceeds in two steps, each requiring only the ability to create a TaskRun, PipelineRun, or ResolutionRequest in a single namespace.
step 1 — arbitrary file read
the attacker creates a ResolutionRequest with a traversal path:
spec:
params:
- name: url
value: https://github.com/tektoncd/catalog
- name: revision
value: main
- name: pathInRepo
value: "../../../../proc/self/mountinfo"
the resolver clones the repository, constructs the path
filepath.Join(cloneDir, "../../../../proc/self/mountinfo"), and reads the file.
the contents are returned base64-encoded in resolutionrequest.status.data.
this reveals container overlay paths, host disk layout, and internal mount structure.
step 2 — ServiceAccount token exfiltration
the attacker targets the well-known token path:
- name: pathInRepo
value: "../../../../var/run/secrets/kubernetes.io/serviceaccount/token"
a valid JWT is returned for
system:serviceaccount:tekton-pipelines-resolvers:tekton-pipelines-resolvers.
in the default RBAC configuration, this ServiceAccount has the following permissions:
| resource | verbs |
|---|---|
| secrets | get, list, watch |
| serviceaccounts | get, list, watch |
| resolutionrequests | get, list, watch, update, patch |
| tasks.tekton.dev | get, list |
| pipelines.tekton.dev | get, list |
the stolen token grants cluster-wide read access to all Secrets across all namespaces — a full privilege escalation from namespace-scoped tenant to cluster-wide secret access.
symlink bypass in initial patch
the vendor prepared an initial fix that validated pathInRepo by rejecting
.. components and absolute paths, then compared
filepath.Abs(repo/path) against the repository root before calling
os.ReadFile().
during review of this patch, 1seal identified that the fix was bypassable via symlinks committed inside an attacker-controlled repository:
the containment check uses filepath.Abs(), which is a lexical
operation — it does not resolve symlinks. however, os.ReadFile()
does follow symlinks. an attacker can commit a symlink inside their
repository:
link -> /var/run/secrets/kubernetes.io/serviceaccount/token
then request pathInRepo=link. the lexical check sees the path as being inside
the clone directory. os.ReadFile() follows the symlink and reads the target
outside the directory. the containment is bypassed.
this was confirmed with a unit test that failed with:
symlink escape still readable: "secret-token-value".
the final fix adds filepath.EvalSymlinks() to resolve the real path before the
containment check, and includes regression tests for symlink escapes, ..
traversal, and absolute paths.
fix
fixed in Tekton Pipelines releases: v1.0.1, v1.3.3, v1.6.1, v1.9.2, v1.10.2. the fix:
- validates
pathInRepoinPopulateDefaultParams()to reject paths containing..components and absolute paths at parameter validation time - adds a containment check in
getFileContent()usingfilepath.EvalSymlinks()to resolve the real path and verify it remains withinrepo.directory(defense in depth) - includes regression tests for
..traversal, absolute paths, and symlink-based escapes from attacker-controlled repositories
| branch | commit |
|---|---|
| main | 961388fcf337 |
| v1.10.x | cdb4e1e97a4f |
| v1.9.x | 3ca7bc6e6dd1 |
| v1.6.x | b1fee65b88aa |
| v1.3.x | 10fa538f9a2b |
| v1.0.x | ec7755031a18 |
users should update to the latest patch release on their branch immediately.
timeline
| date | event |
|---|---|
| 2026-03-02 | reported via GitHub Security Advisory (GHSA-j5q5-j9gm-2w5c) |
| 2026-03-11 | vulnerability confirmed and reproduced by Tekton maintainer |
| 2026-03-11 | initial patch prepared in private fork |
| 2026-03-11 | 1seal review identifies symlink bypass in initial patch |
| 2026-03-11 | maintainer acknowledges bypass, prepares revised fix with EvalSymlinks |
| 2026-03-12 | final fix with symlink containment merged in private fork |
| 2026-03-18 | patched releases published (v1.0.1, v1.3.3, v1.6.1, v1.9.2, v1.10.2) |
| 2026-03-18 | GHSA-j5q5-j9gm-2w5c published |
credit
discovered by Oleh Konko (1seal).
additionally credited: @KoreaSecurity (Finder, per GHSA).
symlink bypass in initial patch identified during fix review by Oleh Konko (1seal).
fix developed by Vincent Demeester (@vdemeester, Tekton maintainer). fix reviewed by Andrea Frittoli (@afrittoli, Tekton maintainer).