Fulcio vulnerable to Server-Side Request Forgery (SSRF) via MetaIssuer Regex Bypass
Description
Fulcio is a certificate authority for issuing code signing certificates for an OpenID Connect (OIDC) identity. Prior to 1.8.5, Fulcio's metaRegex() function uses unanchored regex, allowing attackers to bypass MetaIssuer URL validation and trigger SSRF to arbitrary internal services. Since the SSRF only can trigger GET requests, the request cannot mutate state. The response from the GET request is not returned to the caller so data exfiltration is not possible. A malicious actor could attempt to probe an internal network through Blind SSRF. This vulnerability is fixed in 1.8.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/sigstore/fulcioGo | < 1.8.5 | 1.8.5 |
Affected products
1Patches
1eaae2f2be56dAdd anchors when matching meta issuer regexp (GHSA-59jp-pj84-45mr) (#2263)
4 files changed · +70 −73
pkg/config/config.go+8 −2 modified@@ -140,7 +140,7 @@ type OIDCIssuer struct { SkipEmailVerification bool `json:"SkipEmailVerification,omitempty" yaml:"skip-email-verification,omitempty"` } -func metaRegex(issuer string) (*regexp.Regexp, error) { +func MetaRegex(issuer string) (*regexp.Regexp, error) { // Quote all of the "meta" characters like `.` to avoid // those literal characters in the URL matching any character. // This will ALSO quote `*`, so we replace the quoted version. @@ -151,6 +151,12 @@ func metaRegex(issuer string) (*regexp.Regexp, error) { // "special" characters. replaced := strings.ReplaceAll(quoted, regexp.QuoteMeta("*"), "[-_a-zA-Z0-9]+") + // Add anchors to the beginning and end of the regular expression + // to prevent matching URLs where the issuer is not the host of the URL, + // e.g. http://localhost:3000?https://meta-url-issuer.com/* + // Resolves GHSA-59jp-pj84-45mr + replaced = "^" + replaced + "$" + // Compile into a regular expression. return regexp.Compile(replaced) } @@ -165,7 +171,7 @@ func (fc *FulcioConfig) GetIssuer(issuerURL string) (OIDCIssuer, bool) { } for meta, iss := range fc.MetaIssuers { - re, err := metaRegex(meta) + re, err := MetaRegex(meta) if err != nil { continue // Shouldn't happen, we check parsing the config }
pkg/config/config_test.go+61 −1 modified@@ -114,6 +114,9 @@ func TestMetaURLs(t *testing.T) { "https://oidc.eks.us.west.2.amazonaws.com/id/B02C93B6A2D30341AD01E1B6D48164CB", // Extra slashes "https://oidc.eks.us-west/2.amazonaws.com/id/B02C93B6A2D3/0341AD01E1B6D48164CB", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://oidc.eks.us-west-2.amazonaws.com/id/B02C93B6A2D30341AD01E1B6D48164CB", + "http://localhost/redirect/https://oidc.eks.us-west-2.amazonaws.com/id/B02C93B6A2D30341AD01E1B6D48164CB", }, }, { name: "GKE meta URL", @@ -124,12 +127,69 @@ func TestMetaURLs(t *testing.T) { misses: []string{ // Extra dots "https://container.googleapis.com/v1/projects/mattmoor-credit/locations/us.west1.b/clusters/tenant-cluster", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://container.googleapis.com/v1/projects/mattmoor-credit/locations/us-west1-b/clusters/tenant-cluster", + "http://localhost/redirect/https://container.googleapis.com/v1/projects/mattmoor-credit/locations/us-west1-b/clusters/tenant-cluster", + }, + }, { + name: "Azure meta URL", + issuer: "https://*.oic.prod-aks.azure.com/*", + matches: []string{ + "https://eastus.oic.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + }, + misses: []string{ + // Extra dots + "https://eastus.oic.prod.aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + // Extra slashes + "https://eastus/oic.prod.aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://eastus.oic.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + "http://localhost/redirect/https://eastus.oic.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + }, + }, { + name: "CircleCI meta URL", + issuer: "https://oidc.circleci.com/org/*", + matches: []string{ + "https://oidc.circleci.com/org/my-org", + "https://oidc.circleci.com/org/12345", + }, + misses: []string{ + // Extra slashes + "https://oidc.circleci.com/org/my-org/extra", + // Missing org + "https://oidc.circleci.com/org/", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://oidc.circleci.com/org/my-org", + }, + }, { + name: "Azure prod-aks meta URL", + issuer: "https://oidc.prod-aks.azure.com/*", + matches: []string{ + "https://oidc.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + }, + misses: []string{ + // Extra slashes + "https://oidc.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0/extra", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://oidc.prod-aks.azure.com/ffffffff-eeee-dddd-cccc-bbbbbbbbbbb0", + }, + }, { + name: "GitHub Actions meta URL", + issuer: "https://token.actions.githubusercontent.com/*", + matches: []string{ + "https://token.actions.githubusercontent.com/some-enterprise", + }, + misses: []string{ + // Extra slashes + "https://token.actions.githubusercontent.com/some-enterprise/extra", + // Issuer is not the URL host (GHSA-59jp-pj84-45mr) + "http://localhost?issuer=https://token.actions.githubusercontent.com/some-enterprise", }, }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { - re, err := metaRegex(test.issuer) + re, err := MetaRegex(test.issuer) if err != nil { t.Errorf("metaRegex() = %v", err) }
pkg/identity/base/issuer.go+1 −18 modified@@ -17,8 +17,6 @@ package base import ( "context" "fmt" - "regexp" - "strings" "github.com/google/go-cmp/cmp/cmpopts" "github.com/sigstore/fulcio/pkg/config" @@ -50,24 +48,9 @@ func (e *baseIssuer) Match(_ context.Context, url string) bool { } // If this is a MetaIssuer the issuer URL could be a regex // Check if the regex is valid against the provided url - re, err := metaRegex(e.issuerURL) + re, err := config.MetaRegex(e.issuerURL) if err != nil { return false } return re.MatchString(url) } - -func metaRegex(issuer string) (*regexp.Regexp, error) { - // Quote all of the "meta" characters like `.` to avoid - // those literal characters in the URL matching any character. - // This will ALSO quote `*`, so we replace the quoted version. - quoted := regexp.QuoteMeta(issuer) - - // Replace the quoted `*` with a regular expression that - // will match alpha-numeric parts with common additional - // "special" characters. - replaced := strings.ReplaceAll(quoted, regexp.QuoteMeta("*"), "[-_a-zA-Z0-9]+") - - // Compile into a regular expression. - return regexp.Compile(replaced) -}
pkg/identity/base/issuer_test.go+0 −52 modified@@ -19,58 +19,6 @@ import ( "testing" ) -func TestMetaURLs(t *testing.T) { - tests := []struct { - name string - issuer string - matches []string - misses []string - }{{ - name: "AWS meta URL", - issuer: "https://oidc.eks.*.amazonaws.com/id/*", - matches: []string{ - "https://oidc.eks.us-west-2.amazonaws.com/id/B02C93B6A2D30341AD01E1B6D48164CB", - }, - misses: []string{ - // Extra dots - "https://oidc.eks.us.west.2.amazonaws.com/id/B02C93B6A2D30341AD01E1B6D48164CB", - // Extra slashes - "https://oidc.eks.us-west/2.amazonaws.com/id/B02C93B6A2D3/0341AD01E1B6D48164CB", - }, - }, { - name: "GKE meta URL", - issuer: "https://container.googleapis.com/v1/projects/*/locations/*/clusters/*", - matches: []string{ - "https://container.googleapis.com/v1/projects/mattmoor-credit/locations/us-west1-b/clusters/tenant-cluster", - }, - misses: []string{ - // Extra dots - "https://container.googleapis.com/v1/projects/mattmoor-credit/locations/us.west1.b/clusters/tenant-cluster", - }, - }} - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - re, err := metaRegex(test.issuer) - if err != nil { - t.Errorf("metaRegex() = %v", err) - } - - for _, match := range test.matches { - if !re.MatchString(match) { - t.Errorf("MatchString(%q) = false, wanted true", match) - } - } - - for _, miss := range test.misses { - if re.MatchString(miss) { - t.Errorf("MatchString(%q) = true, wanted false", miss) - } - } - }) - } -} - func TestMatch(t *testing.T) { tests := []struct { description string
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
4- github.com/advisories/GHSA-59jp-pj84-45mrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22772ghsaADVISORY
- github.com/sigstore/fulcio/commit/eaae2f2be56df9dea5f9b439ec81bedae4c0978dghsax_refsource_MISCWEB
- github.com/sigstore/fulcio/security/advisories/GHSA-59jp-pj84-45mrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.