VYPR
Medium severity6.5NVD Advisory· Published Apr 21, 2026· Updated May 1, 2026

CVE-2026-25542

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.

PackageAffected versionsPatched versions
github.com/tektoncd/pipelineGo
>= 0.43.0, < 1.11.01.11.0

Affected products

1

Patches

2
b8905600322a

fix: strip resolver prefixes and use non-capturing group for pattern anchoring

https://github.com/tektoncd/pipelineVincent DemeesterMar 18, 2026via ghsa
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)
    +			}
    +		})
    +	}
    +}
    
2c398711e6e9

perf(pipelinerun): hoist VerificationPolicy list out of per-task loop in resolvePipelineState

https://github.com/tektoncd/pipelineAnkur SinhaMar 17, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.