CVE-2026-25542
Description
Tekton Pipelines project provides k8s-style resources for declaring CI/CD-style pipelines. From 0.43.0 to 1.11.0, trusted resources verification policies match a resource source string (refSource.URI) against spec.resources[].pattern using regexp.MatchString. In Go, regexp.MatchString reports a match if the pattern matches anywhere in the string, so common unanchored patterns (including examples in tekton documentation) can be bypassed by attacker-controlled source strings that contain the trusted pattern as a substring. This can cause an unintended policy match and change which verification mode/keys apply.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/tektoncd/pipelineGo | >= 0.43.0, < 1.11.0 | 1.11.0 |
Affected products
1- cpe:2.3:a:linuxfoundation:tekton_pipelines:*:*:*:*:*:go:*:*Range: >=0.43.0,<1.11.0
Patches
2b8905600322afix: strip resolver prefixes and use non-capturing group for pattern anchoring
2 files changed · +228 −1
pkg/trustedresources/verify.go+43 −1 modified@@ -23,6 +23,7 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/sigstore/sigstore/pkg/signature" "github.com/tektoncd/pipeline/pkg/apis/config" @@ -117,9 +118,14 @@ func VerifyPipeline(ctx context.Context, pipelineObj *v1beta1.Pipeline, k8s kube // getMatchedPolicies filters out the policies by checking if the resource url (source) is matching any of the `patterns` in the `resources` list. func getMatchedPolicies(resourceName string, source string, policies []*v1alpha1.VerificationPolicy) ([]*v1alpha1.VerificationPolicy, error) { matchedPolicies := []*v1alpha1.VerificationPolicy{} + // Strip known resolver prefixes before matching so that patterns + // written without the prefix (e.g. "https://github.com/…") still + // work after we anchor them for full-string matching. + normalizedSource := stripResolverPrefix(source) for _, p := range policies { for _, r := range p.Spec.Resources { - matching, err := regexp.MatchString(r.Pattern, source) + pattern := anchorPattern(r.Pattern) + matching, err := regexp.MatchString(pattern, normalizedSource) if err != nil { // FixMe: changing %v to %w breaks integration tests. return matchedPolicies, fmt.Errorf("%v: %w", err, ErrRegexMatch) //nolint:errorlint @@ -136,6 +142,42 @@ func getMatchedPolicies(resourceName string, source string, policies []*v1alpha1 return matchedPolicies, nil } +// anchorPattern wraps a pattern with ^(?:…)$ when it is not already +// anchored. This forces full-string matching so that unanchored +// patterns cannot be bypassed by embedding the trusted URL as a +// substring of a malicious source URI. +func anchorPattern(pattern string) string { + hasStart := strings.HasPrefix(pattern, "^") + hasEnd := strings.HasSuffix(pattern, "$") + if hasStart && hasEnd { + return pattern + } + if !hasStart { + pattern = "^(?:" + pattern + } else { + // Already has ^, wrap the rest in a non-capturing group. + pattern = "^(?:" + pattern[1:] + } + if !hasEnd { + pattern = pattern + ")$" + } else { + // Already has $, close the group before it. + pattern = pattern[:len(pattern)-1] + ")$" + } + return pattern +} + +// stripResolverPrefix removes known URI scheme prefixes added by +// resolvers before policy matching. The git resolver prepends "git+" +// per the SPDX convention (see spdxGit in +// pkg/resolution/resolver/git/resolver.go). +func stripResolverPrefix(uri string) string { + if strings.HasPrefix(uri, "git+") { + return uri[4:] + } + return uri +} + // verifyResource verifies resource which implements metav1.Object by provided signature and public keys from verification policies. // For matched policies, `verifyResource“ will adopt the following rules to do verification: // 1. If multiple policies match, the resource must satisfy all the "enforce" policies to pass verification. The matching "enforce" policies are evaluated using AND logic.
pkg/trustedresources/verify_regex_bypass_control_test.go+185 −0 added@@ -0,0 +1,185 @@ +package trustedresources + +import ( + "errors" + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" +) + +func TestGetMatchedPolicies_RegexBypass(t *testing.T) { + policy := &v1alpha1.VerificationPolicy{ + Spec: v1alpha1.VerificationPolicySpec{ + Resources: []v1alpha1.ResourcePattern{{ + Pattern: "https://github.com/tektoncd/catalog.git", + }}, + }, + } + policies := []*v1alpha1.VerificationPolicy{policy} + + tests := []struct { + name string + source string + wantMatch bool + description string + }{{ + name: "malicious source with trusted URL as query param is rejected", + source: "https://evil.com/?x=https://github.com/tektoncd/catalog.git", + wantMatch: false, + description: "substring bypass via query parameter", + }, { + name: "malicious source with trusted URL as path component is rejected", + source: "https://evil.com/https://github.com/tektoncd/catalog.git/foo", + wantMatch: false, + description: "substring bypass via path embedding", + }, { + name: "exact match without prefix succeeds", + source: "https://github.com/tektoncd/catalog.git", + wantMatch: true, + description: "exact source matches unanchored pattern", + }, { + name: "git+ prefixed source matches after prefix stripping", + source: "git+https://github.com/tektoncd/catalog.git", + wantMatch: true, + description: "git resolver spdx prefix is stripped before matching", + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + matched, err := getMatchedPolicies("test-resource", tc.source, policies) + if tc.wantMatch { + if err != nil { + t.Fatalf("expected match for source %q, got err: %v", tc.source, err) + } + if len(matched) != 1 { + t.Fatalf("expected 1 matched policy, got %d", len(matched)) + } + } else { + if err == nil { + t.Fatalf("expected no match for source %q, but got a match", tc.source) + } + if !errors.Is(err, ErrNoMatchedPolicies) { + t.Fatalf("expected ErrNoMatchedPolicies, got: %v", err) + } + } + }) + } +} + +func TestGetMatchedPolicies_AlreadyAnchoredPatterns(t *testing.T) { + tests := []struct { + name string + pattern string + source string + wantMatch bool + }{{ + name: "fully anchored pattern still works", + pattern: "^https://github\\.com/tektoncd/catalog\\.git$", + source: "https://github.com/tektoncd/catalog.git", + wantMatch: true, + }, { + name: "start-anchored pattern gets end-anchored", + pattern: "^https://github.com/tektoncd/.*", + source: "https://github.com/tektoncd/catalog.git", + wantMatch: true, + }, { + name: "end-anchored pattern gets start-anchored", + pattern: ".*catalog.git$", + source: "https://github.com/tektoncd/catalog.git", + wantMatch: true, + }, { + name: "wildcard pattern matches everything", + pattern: ".*", + source: "https://anything.example.com", + wantMatch: true, + }, { + name: "git+ prefix stripped before matching anchored pattern", + pattern: "^https://github\\.com/tektoncd/catalog\\.git$", + source: "git+https://github.com/tektoncd/catalog.git", + wantMatch: true, + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + policy := &v1alpha1.VerificationPolicy{ + Spec: v1alpha1.VerificationPolicySpec{ + Resources: []v1alpha1.ResourcePattern{{ + Pattern: tc.pattern, + }}, + }, + } + matched, err := getMatchedPolicies("test-resource", tc.source, []*v1alpha1.VerificationPolicy{policy}) + if tc.wantMatch { + if err != nil { + t.Fatalf("expected match for pattern %q against source %q, got err: %v", tc.pattern, tc.source, err) + } + if len(matched) != 1 { + t.Fatalf("expected 1 matched policy, got %d", len(matched)) + } + } else { + if err == nil { + t.Fatalf("expected no match for pattern %q against source %q, but got a match", tc.pattern, tc.source) + } + } + }) + } +} + +func TestAnchorPattern(t *testing.T) { + tests := []struct { + input string + want string + }{{ + input: "https://github.com/tektoncd/catalog.git", + want: "^(?:https://github.com/tektoncd/catalog.git)$", + }, { + input: "^https://github.com/tektoncd/.*", + want: "^(?:https://github.com/tektoncd/.*)$", + }, { + input: ".*catalog.git$", + want: "^(?:.*catalog.git)$", + }, { + input: "^https://github\\.com/tektoncd/catalog\\.git$", + want: "^https://github\\.com/tektoncd/catalog\\.git$", + }, { + input: ".*", + want: "^(?:.*)$", + }} + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + got := anchorPattern(tc.input) + if got != tc.want { + t.Errorf("anchorPattern(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestStripResolverPrefix(t *testing.T) { + tests := []struct { + input string + want string + }{{ + input: "git+https://github.com/tektoncd/catalog.git", + want: "https://github.com/tektoncd/catalog.git", + }, { + input: "https://github.com/tektoncd/catalog.git", + want: "https://github.com/tektoncd/catalog.git", + }, { + input: "gcr.io/tekton-releases/catalog/upstream/git-clone", + want: "gcr.io/tekton-releases/catalog/upstream/git-clone", + }, { + input: "", + want: "", + }} + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + got := stripResolverPrefix(tc.input) + if got != tc.want { + t.Errorf("stripResolverPrefix(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +}
2c398711e6e9perf(pipelinerun): hoist VerificationPolicy list out of per-task loop in resolvePipelineState
1 file changed · +7 −6
pkg/reconciler/pipelinerun/pipelinerun.go+7 −6 modified@@ -367,6 +367,13 @@ func (c *Reconciler) resolvePipelineState( ) (resources.PipelineRunState, error) { ctx, span := c.tracerProvider.Tracer(TracerName).Start(ctx, "resolvePipelineState") defer span.End() + + // List VerificationPolicies once per reconcile for trusted resources (used by all pipeline tasks). + vp, err := c.verificationPolicyLister.VerificationPolicies(pr.Namespace).List(labels.Everything()) + if err != nil { + return nil, fmt.Errorf("failed to list VerificationPolicies from namespace %s with error %w", pr.Namespace, err) + } + // Resolve each pipeline task individually because they each could have a different reference context (remote or local). for _, pipelineTask := range pipelineTasks { // We need the TaskRun name to ensure that we don't perform an additional remote resolution request for a PipelineTask @@ -377,12 +384,6 @@ func (c *Reconciler) resolvePipelineState( pr.Name, ) - // list VerificationPolicies for trusted resources - vp, err := c.verificationPolicyLister.VerificationPolicies(pr.Namespace).List(labels.Everything()) - if err != nil { - return nil, fmt.Errorf("failed to list VerificationPolicies from namespace %s with error %w", pr.Namespace, err) - } - getChildPipelineRunFunc := func(name string) (*v1.PipelineRun, error) { return c.pipelineRunLister.PipelineRuns(pr.Namespace).Get(name) }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/tektoncd/pipeline/commit/b8905600322aa86327baae0a7c04d6cf1207362anvdPatchWEB
- github.com/tektoncd/pipeline/security/advisories/GHSA-rmx9-2pp3-xhcrnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-rmx9-2pp3-xhcrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25542ghsaADVISORY
- github.com/tektoncd/pipeline/commit/2c398711e6e9e232180508f0648425a8ea34dc9eghsaWEB
- github.com/tektoncd/pipeline/releases/tag/v1.11.0ghsaWEB
News mentions
0No linked articles in our index yet.