VYPR
High severity8.6OSV Advisory· Published Jun 26, 2025· Updated Apr 15, 2026

CVE-2025-52477

CVE-2025-52477

Description

Octo-STS is a GitHub App that acts like a Security Token Service (STS) for the GitHub API. Octo-STS versions before v0.5.3 are vulnerable to unauthenticated SSRF by abusing fields in OpenID Connect tokens. Malicious tokens were shown to trigger internal network requests which could reflect error logs with sensitive information. Upgrade to v0.5.3 to resolve this issue. This version includes patch sets to sanitize input and redact logging.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/octo-sts/appGo
< 0.5.30.5.3

Affected products

1

Patches

2
0f177fde54f9

Merge commit from fork

https://github.com/octo-sts/appCarlos Tadeu Panato JuniorJun 26, 2025via ghsa
1 file changed · +15 7
  • pkg/octosts/octosts.go+15 7 modified
    @@ -111,7 +111,8 @@ func (s *sts) Exchange(ctx context.Context, request *pboidc.ExchangeRequest) (_
     	// Validate the Bearer token.
     	issuer, err := apiauth.ExtractIssuer(bearer)
     	if err != nil {
    -		return nil, status.Errorf(codes.InvalidArgument, "invalid bearer token: %v", err)
    +		clog.FromContext(ctx).Debugf("invalid bearer token: %v", err)
    +		return nil, status.Error(codes.InvalidArgument, "invalid bearer token")
     	}
     
     	// Validate issuer format
    @@ -122,7 +123,8 @@ func (s *sts) Exchange(ctx context.Context, request *pboidc.ExchangeRequest) (_
     	// Fetch the provider from the cache or create a new one and add to the cache
     	p, err := provider.Get(ctx, issuer)
     	if err != nil {
    -		return nil, status.Errorf(codes.InvalidArgument, "unable to fetch or create the provider: %v", err)
    +		clog.FromContext(ctx).Debugf("unable to fetch or create the provider: %v", err)
    +		return nil, status.Error(codes.InvalidArgument, "unable to fetch or create the provider")
     	}
     
     	verifier := p.Verifier(&oidc.Config{
    @@ -131,7 +133,8 @@ func (s *sts) Exchange(ctx context.Context, request *pboidc.ExchangeRequest) (_
     	})
     	tok, err := verifier.Verify(ctx, bearer)
     	if err != nil {
    -		return nil, status.Errorf(codes.Unauthenticated, "unable to validate token: %v", err)
    +		clog.FromContext(ctx).Debugf("unable to validate token: %v", err)
    +		return nil, status.Error(codes.Unauthenticated, "unable to verify bearer token")
     	}
     	// This is typically overwritten below, but we populate this here to enrich
     	// certain error paths with the issuer and subject.
    @@ -170,18 +173,23 @@ func (s *sts) Exchange(ctx context.Context, request *pboidc.ExchangeRequest) (_
     			// body typically, so extract that.
     			if herr.Response.StatusCode == http.StatusUnprocessableEntity {
     				if body, err := io.ReadAll(herr.Response.Body); err == nil {
    -					return nil, status.Errorf(codes.PermissionDenied, "token exchange failure: %s", body)
    +					clog.FromContext(ctx).Debugf("token exchange failure (StatusUnprocessableEntity): %s", body)
    +					return nil, status.Error(codes.PermissionDenied, "token exchange failure (StatusUnprocessableEntity)")
     				}
     			} else {
     				body, err := httputil.DumpResponse(herr.Response, true)
     				if err == nil {
    -					clog.FromContext(ctx).Warnf("token exchange failure: %s", body)
    +					clog.FromContext(ctx).Warn("token exchange failure, redacting body in logs")
    +					// Log the response body in debug mode only, as it may contain sensitive information.
    +					clog.FromContext(ctx).Debugf("token exchange failure: %s", body)
     				}
     			}
     		} else {
    -			clog.FromContext(ctx).Warnf("token exchange failure: %v", err)
    +			clog.FromContext(ctx).Debugf("token exchange failure: %v", err)
    +			clog.FromContext(ctx).Warn("token exchange failure, redacting error in logs")
     		}
    -		return nil, status.Errorf(codes.Internal, "failed to get token: %v", err)
    +		clog.FromContext(ctx).Debugf("failed to get token: %v", err)
    +		return nil, status.Error(codes.Internal, "failed to get token")
     	}
     
     	// Compute the SHA256 hash of the token and store the hex-encoded value into e.TokenSHA256
    
b3976e39bd8c

Merge commit from fork

https://github.com/octo-sts/appMaxime GréauJun 26, 2025via ghsa
5 files changed · +772 0
  • pkg/octosts/octosts.go+6 0 modified
    @@ -31,6 +31,7 @@ import (
     	apiauth "chainguard.dev/sdk/auth"
     	pboidc "chainguard.dev/sdk/proto/platform/oidc/v1"
     	"github.com/chainguard-dev/clog"
    +	"github.com/octo-sts/app/pkg/oidcvalidate"
     	"github.com/octo-sts/app/pkg/provider"
     )
     
    @@ -113,6 +114,11 @@ func (s *sts) Exchange(ctx context.Context, request *pboidc.ExchangeRequest) (_
     		return nil, status.Errorf(codes.InvalidArgument, "invalid bearer token: %v", err)
     	}
     
    +	// Validate issuer format
    +	if !oidcvalidate.IsValidIssuer(issuer) {
    +		return nil, status.Error(codes.InvalidArgument, "invalid issuer format")
    +	}
    +
     	// Fetch the provider from the cache or create a new one and add to the cache
     	p, err := provider.Get(ctx, issuer)
     	if err != nil {
    
  • pkg/octosts/trust_policy.go+14 0 modified
    @@ -11,6 +11,7 @@ import (
     
     	"github.com/coreos/go-oidc/v3/oidc"
     	"github.com/google/go-github/v72/github"
    +	"github.com/octo-sts/app/pkg/oidcvalidate"
     	"google.golang.org/grpc/codes"
     	"google.golang.org/grpc/status"
     )
    @@ -115,6 +116,19 @@ func (tp *TrustPolicy) CheckToken(token *oidc.IDToken, domain string) (Actor, er
     		return act, status.Errorf(codes.Internal, "trust policy: not compiled")
     	}
     
    +	// Validate critical token fields
    +	if !oidcvalidate.IsValidIssuer(token.Issuer) {
    +		return act, status.Errorf(codes.InvalidArgument, "invalid issuer in token")
    +	}
    +	if !oidcvalidate.IsValidSubject(token.Subject) {
    +		return act, status.Errorf(codes.InvalidArgument, "invalid subject in token")
    +	}
    +	for _, aud := range token.Audience {
    +		if !oidcvalidate.IsValidAudience(aud) {
    +			return act, status.Errorf(codes.InvalidArgument, "invalid audience in token")
    +		}
    +	}
    +
     	// Check the issuer.
     	switch {
     	case tp.issuerPattern != nil:
    
  • pkg/oidcvalidate/validate.go+223 0 added
    @@ -0,0 +1,223 @@
    +// Copyright 2025 Chainguard, Inc.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package oidcvalidate
    +
    +import (
    +	"net/url"
    +	"regexp"
    +	"strings"
    +	"unicode"
    +	"unicode/utf8"
    +)
    +
    +const (
    +	// controlCharsAndWhitespace contains ASCII control characters (0x00-0x1f) plus whitespace
    +	controlCharsAndWhitespace = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f \t\n\r"
    +)
    +
    +// IsValidIssuer validates an OIDC issuer according to RFC 8414 and OpenID Connect Core 1.0:
    +// - Must use HTTPS scheme (except localhost for testing)
    +// - Must be a valid URL without query string or fragment
    +// - Must not end with a slash
    +// - Must have a valid hostname
    +// - Length constraints for security
    +func IsValidIssuer(iss string) bool {
    +	// Basic length check
    +	if len(iss) == 0 || utf8.RuneCountInString(iss) > 255 {
    +		return false
    +	}
    +
    +	// Parse as URL
    +	parsedURL, err := url.Parse(iss)
    +	if err != nil {
    +		return false
    +	}
    +
    +	// Must use HTTPS (allow HTTP only for localhost/127.0.0.1 for development/testing environments)
    +	if parsedURL.Scheme != "https" {
    +		if parsedURL.Scheme == "http" {
    +			host := parsedURL.Hostname()
    +			if host != "localhost" && host != "127.0.0.1" && host != "::1" {
    +				return false
    +			}
    +		} else {
    +			// Reject any scheme that is not HTTPS or HTTP
    +			return false
    +		}
    +	}
    +
    +	// Must not contain query string or fragment (RFC 8414)
    +	// Check both parsed components and raw string for fragments/queries
    +	if parsedURL.RawQuery != "" || parsedURL.Fragment != "" {
    +		return false
    +	}
    +	if strings.ContainsAny(iss, "?#") {
    +		return false
    +	}
    +
    +	// Must have a valid hostname
    +	if parsedURL.Host == "" {
    +		return false
    +	}
    +
    +	// Reject URLs with userinfo (user:pass@host)
    +	if parsedURL.User != nil {
    +		return false
    +	}
    +
    +	// Comprehensive hostname validation
    +	if !isValidHostname(parsedURL.Hostname()) {
    +		return false
    +	}
    +
    +	// Path validation - if present, must be valid
    +	if parsedURL.Path != "" {
    +		// Reject paths with .. or other suspicious patterns
    +		if strings.Contains(parsedURL.Path, "..") {
    +			return false
    +		}
    +		// Must start with / if path is present
    +		if !strings.HasPrefix(parsedURL.Path, "/") {
    +			return false
    +		}
    +		// Reject multiple consecutive slashes (e.g., //, ///)
    +		if strings.Contains(parsedURL.Path, "//") {
    +			return false
    +		}
    +		// Reject multiple consecutive tildes (e.g., ~~, ~~~)
    +		if strings.Contains(parsedURL.Path, "~~") {
    +			return false
    +		}
    +		// Reject paths ending with tilde (could indicate backup files)
    +		if strings.HasSuffix(parsedURL.Path, "~") {
    +			return false
    +		}
    +		// Strict path character validation - only allow safe characters
    +		// Allow: letters, digits, hyphens, dots, underscores, tildes, forward slashes
    +		pathRe := regexp.MustCompile(`^[a-zA-Z0-9\-._~/]+$`)
    +		if !pathRe.MatchString(parsedURL.Path) {
    +			return false
    +		}
    +		// Additional validation: each path segment should be reasonable
    +		segments := strings.Split(parsedURL.Path, "/")
    +		for _, segment := range segments {
    +			if segment == "" {
    +				continue // Skip empty segments (like the first one after leading /)
    +			}
    +			// Reject segments that are just dots or tildes
    +			if segment == "." || segment == ".." || segment == "~" {
    +				return false
    +			}
    +			// Reject very long path segments to prevent buffer overflows, path traversal,
    +			// and resource exhaustion. RFC 3986 sets no explicit limit, but a 150-character
    +			// cap is reasonable for legitimate paths in most apps.
    +			if len(segment) > 150 {
    +				return false
    +			}
    +		}
    +	}
    +
    +	return true
    +}
    +
    +// IsValidSubject validates a subject identifier according to OpenID Connect Core 1.0:
    +// - Must not be empty (REQUIRED)
    +// - Must be a string with maximum length of 255 ASCII characters
    +// - Must not contain whitespace or control characters
    +// - Case sensitive string comparison
    +func IsValidSubject(sub string) bool {
    +	// Must not be empty (OIDC Core requirement)
    +	if sub == "" {
    +		return false
    +	}
    +
    +	// Length validation - OIDC recommends max 255 ASCII characters
    +	if utf8.RuneCountInString(sub) > 255 {
    +		return false
    +	}
    +
    +	// Must not contain control characters, whitespace, or newlines
    +	// These could interfere with logging, storage, or protocol handling
    +	if strings.ContainsAny(sub, controlCharsAndWhitespace) {
    +		return false
    +	}
    +
    +	// Reject obviously problematic characters that could cause issues
    +	// in various contexts (JSON, XML, SQL, shell, etc.)
    +	// Allow: | : / @ - (commonly used by Auth0, GitHub Actions, etc.)
    +	if strings.ContainsAny(sub, "\"'`\\<>;&$(){}[]") {
    +		return false
    +	}
    +
    +	// The subject MUST be valid UTF-8 (already ensured by Go string type)
    +	// and should be printable characters only
    +	for _, r := range sub {
    +		// Reject non-printable characters (except those already caught above)
    +		if !unicode.IsPrint(r) {
    +			return false
    +		}
    +	}
    +
    +	return true
    +}
    +
    +// IsValidAudience validates an audience identifier according to OpenID Connect Core 1.0:
    +// - Must not be empty (audience is REQUIRED)
    +// - Should be a URI or an arbitrary string that uniquely identifies the audience
    +// - Case sensitive string comparison
    +// - Maximum length of 255 characters for security
    +// - Must not contain control characters or injection-prone characters
    +func IsValidAudience(aud string) bool {
    +	// Must not be empty (OIDC requirement)
    +	if aud == "" {
    +		return false
    +	}
    +
    +	// Length validation for security
    +	if utf8.RuneCountInString(aud) > 255 {
    +		return false
    +	}
    +
    +	// Must not contain control characters, whitespace that could cause parsing issues
    +	if strings.ContainsAny(aud, controlCharsAndWhitespace) {
    +		return false
    +	}
    +
    +	// Reject characters that could cause injection issues or confusion
    +	if strings.ContainsAny(aud, "\"'`\\<>;|&$(){}[]@") {
    +		return false
    +	}
    +
    +	// Audience should be printable characters only
    +	for _, r := range aud {
    +		if !unicode.IsPrint(r) {
    +			return false
    +		}
    +	}
    +
    +	return true
    +}
    +
    +// isValidHostname validates hostnames against homograph attacks and Unicode confusion
    +func isValidHostname(hostname string) bool {
    +	// Empty hostname is invalid
    +	if hostname == "" {
    +		return false
    +	}
    +
    +	// Reject control characters, whitespace, tabs, newlines
    +	if strings.ContainsAny(hostname, controlCharsAndWhitespace) {
    +		return false
    +	}
    +
    +	// Reject Unicode characters to prevent homograph attacks
    +	// Examples: exämple.com (ä), eⓍample.com (Ⓧ), еxample.com (Cyrillic е)
    +	for _, r := range hostname {
    +		if r > 127 {
    +			return false
    +		}
    +	}
    +
    +	return true
    +}
    
  • pkg/oidcvalidate/validate_test.go+521 0 added
    @@ -0,0 +1,521 @@
    +// Copyright 2025 Chainguard, Inc.
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package oidcvalidate
    +
    +import (
    +	"strings"
    +	"testing"
    +)
    +
    +func TestIsValidIssuer(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		issuer   string
    +		expected bool
    +	}{
    +		// Valid cases
    +		{
    +			name:     "valid HTTPS issuer",
    +			issuer:   "https://example.com",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid HTTPS issuer with path",
    +			issuer:   "https://example.com/path",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid HTTPS issuer with nested path",
    +			issuer:   "https://example.com/auth/realms/master",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid HTTPS issuer with port",
    +			issuer:   "https://example.com:8443",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid localhost HTTP (testing)",
    +			issuer:   "http://localhost:8080",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid 127.0.0.1 HTTP (testing)",
    +			issuer:   "http://127.0.0.1:8080",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid IPv6 localhost HTTP (testing)",
    +			issuer:   "http://[::1]:8080",
    +			expected: true,
    +		},
    +
    +		// Invalid cases - basic validation
    +		{
    +			name:     "empty issuer",
    +			issuer:   "",
    +			expected: false,
    +		},
    +		{
    +			name:     "invalid URL",
    +			issuer:   "not-a-url",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer too long",
    +			issuer:   "https://" + strings.Repeat("a", 300) + ".com",
    +			expected: false,
    +		},
    +
    +		// Invalid cases - scheme validation
    +		{
    +			name:     "HTTP issuer (not localhost)",
    +			issuer:   "http://example.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "FTP scheme",
    +			issuer:   "ftp://example.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "missing scheme",
    +			issuer:   "example.com",
    +			expected: false,
    +		},
    +
    +		// Valid cases - trailing slash (real-world compatibility)
    +		{
    +			name:     "issuer with trailing slash",
    +			issuer:   "https://example.com/",
    +			expected: true,
    +		},
    +		{
    +			name:     "issuer with path and trailing slash",
    +			issuer:   "https://example.com/path/",
    +			expected: true,
    +		},
    +
    +		// Invalid cases - query and fragment (RFC 8414 violation)
    +		{
    +			name:     "issuer with query parameter",
    +			issuer:   "https://example.com?param=value",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with fragment",
    +			issuer:   "https://example.com#fragment",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with path query and fragment",
    +			issuer:   "https://example.com/path?query=value#fragment",
    +			expected: false,
    +		},
    +		{
    +			name:     "endpoint manipulation with fragment",
    +			issuer:   "https://example.com/controllablepath#",
    +			expected: false,
    +		},
    +
    +		// Invalid cases - userinfo
    +		{
    +			name:     "issuer with userinfo",
    +			issuer:   "https://user:pass@example.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with username only",
    +			issuer:   "https://user@example.com",
    +			expected: false,
    +		},
    +
    +		// Invalid cases - path traversal
    +		{
    +			name:     "issuer with path traversal",
    +			issuer:   "https://example.com/../admin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with path traversal in middle",
    +			issuer:   "https://example.com/auth/../admin",
    +			expected: false,
    +		},
    +
    +		// Invalid cases - control characters
    +		{
    +			name:     "issuer with newline in hostname",
    +			issuer:   "https://example\n.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with tab in hostname",
    +			issuer:   "https://example\t.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with space in hostname",
    +			issuer:   "https://example .com",
    +			expected: false,
    +		},
    +
    +		// Invalid cases - malformed URLs
    +		{
    +			name:     "issuer without host",
    +			issuer:   "https://",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with invalid characters in path",
    +			issuer:   "https://example.com/<script>",
    +			expected: false,
    +		},
    +
    +		// Real-world attack scenarios
    +		{
    +			name:     "issuer with empty fragment (original attack)",
    +			issuer:   "https://example.com/controllablepath#",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with percent encoding",
    +			issuer:   "https://example.com/path%2F..%2Fadmin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with normal path",
    +			issuer:   "https://example.com/admin",
    +			expected: true,
    +		},
    +		{
    +			name:     "issuer with port and path",
    +			issuer:   "https://keycloak.example.com:8443/auth/realms/master",
    +			expected: true,
    +		},
    +
    +		// Path regex edge cases
    +		{
    +			name:     "issuer with double slashes in path",
    +			issuer:   "https://example.com//admin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with triple slashes in path",
    +			issuer:   "https://example.com///admin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with double tildes",
    +			issuer:   "https://example.com/path~~backup",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer ending with tilde (backup file)",
    +			issuer:   "https://example.com/config~",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with single dot segment",
    +			issuer:   "https://example.com/./admin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with tilde segment",
    +			issuer:   "https://example.com/~/admin",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with very long path segment",
    +			issuer:   "https://example.com/" + strings.Repeat("a", 151),
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with valid single tilde in path",
    +			issuer:   "https://example.com/user~name/path",
    +			expected: true,
    +		},
    +
    +		// Homograph/IDN attacks
    +		{
    +			name:     "issuer with Unicode characters (umlaut)",
    +			issuer:   "https://exämple.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with Unicode circled characters",
    +			issuer:   "https://eⓍample.com",
    +			expected: false,
    +		},
    +		{
    +			name:     "issuer with Cyrillic characters",
    +			issuer:   "https://еxample.com", // Cyrillic 'е' instead of Latin 'e'
    +			expected: false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			result := IsValidIssuer(tt.issuer)
    +			if result != tt.expected {
    +				t.Errorf("IsValidIssuer(%q) = %v, expected %v", tt.issuer, result, tt.expected)
    +			}
    +		})
    +	}
    +}
    +
    +func TestIsValidSubject(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		subject  string
    +		expected bool
    +	}{
    +		// Valid subjects according to OIDC spec
    +		{
    +			name:     "valid alphanumeric subject",
    +			subject:  "user123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid UUID subject",
    +			subject:  "550e8400-e29b-41d4-a716-446655440000",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid numeric subject",
    +			subject:  "1234567890",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid subject with hyphen and underscore",
    +			subject:  "user-name_123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid subject with dots",
    +			subject:  "user.name.123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid long subject (within limit)",
    +			subject:  strings.Repeat("a", 255),
    +			expected: true,
    +		},
    +		{
    +			name:     "valid mixed case subject",
    +			subject:  "UserName123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid subject with plus",
    +			subject:  "user+tag",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid subject with equals",
    +			subject:  "user=value",
    +			expected: true,
    +		},
    +
    +		// Real-world OIDC provider examples
    +		// Google OIDC: https://developers.google.com/identity/openid-connect/openid-connect
    +		{
    +			name:     "Google subject identifier",
    +			subject:  "10769150350006150715113082367",
    +			expected: true,
    +		},
    +		// GitHub Actions OIDC: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
    +		{
    +			name:     "GitHub Actions subject - repo ref",
    +			subject:  "repo:octo-org/octo-repo:ref:refs/heads/main",
    +			expected: true,
    +		},
    +		// Okta OIDC: https://developer.okta.com/docs/reference/api/oidc/
    +		{
    +			name:     "Okta subject identifier - with pipe",
    +			subject:  "okta|00uhzsq8pw5e6bWGe0h7",
    +			expected: true,
    +		},
    +		// OIDC Core 1.0 Specification: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
    +		{
    +			name:     "OIDC spec example - numeric",
    +			subject:  "24400320",
    +			expected: true,
    +		},
    +		{
    +			name:     "OIDC spec example - alphanumeric",
    +			subject:  "AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4",
    +			expected: true,
    +		},
    +
    +		// Invalid subjects - OIDC violations
    +		{
    +			name:     "empty subject (OIDC violation)",
    +			subject:  "",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject too long (>255 chars)",
    +			subject:  strings.Repeat("a", 256),
    +			expected: false,
    +		},
    +
    +		// Invalid subjects - whitespace and control characters
    +		{
    +			name:     "subject with space",
    +			subject:  "user 123",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with tab",
    +			subject:  "user\t123",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with newline",
    +			subject:  "user123\n",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with carriage return",
    +			subject:  "user123\r",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with null byte",
    +			subject:  "user\x00123",
    +			expected: false,
    +		},
    +
    +		// Invalid subjects - injection risks
    +		{
    +			name:     "subject with single quote",
    +			subject:  "user'123",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with double quote",
    +			subject:  "user\"123",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with script tags",
    +			subject:  "user<script>alert(1)</script>",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with brackets",
    +			subject:  "user[123]",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with braces",
    +			subject:  "user{123}",
    +			expected: false,
    +		},
    +		{
    +			name:     "subject with semicolon",
    +			subject:  "user;123",
    +			expected: false,
    +		},
    +
    +		// Edge cases
    +		{
    +			name:     "subject with Unicode",
    +			subject:  "用户123",
    +			expected: true,
    +		},
    +		{
    +			name:     "subject with emoji",
    +			subject:  "user😀123",
    +			expected: true,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			result := IsValidSubject(tt.subject)
    +			if result != tt.expected {
    +				t.Errorf("IsValidSubject(%q) = %v, expected %v", tt.subject, result, tt.expected)
    +			}
    +		})
    +	}
    +}
    +
    +func TestIsValidAudience(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		audience string
    +		expected bool
    +	}{
    +		{
    +			name:     "valid audience",
    +			audience: "service123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid audience with hyphen",
    +			audience: "service-123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid audience with underscore",
    +			audience: "service_123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid audience with dot",
    +			audience: "service.123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid audience with colon",
    +			audience: "service:123",
    +			expected: true,
    +		},
    +		{
    +			name:     "valid audience with slash",
    +			audience: "service/path",
    +			expected: true,
    +		},
    +		{
    +			name:     "empty audience",
    +			audience: "",
    +			expected: false,
    +		},
    +		{
    +			name:     "audience too long",
    +			audience: string(make([]byte, 200)),
    +			expected: false,
    +		},
    +		{
    +			name:     "audience with space",
    +			audience: "service 123",
    +			expected: false,
    +		},
    +		{
    +			name:     "audience with newline",
    +			audience: "service123\n",
    +			expected: false,
    +		},
    +		{
    +			name:     "audience with special characters",
    +			audience: "service<script>",
    +			expected: false,
    +		},
    +		{
    +			name:     "audience with at sign",
    +			audience: "service@domain",
    +			expected: false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			result := IsValidAudience(tt.audience)
    +			if result != tt.expected {
    +				t.Errorf("IsValidAudience(%q) = %v, expected %v", tt.audience, result, tt.expected)
    +			}
    +		})
    +	}
    +}
    
  • pkg/provider/provider.go+8 0 modified
    @@ -17,6 +17,7 @@ import (
     	"github.com/coreos/go-oidc/v3/oidc"
     	lru "github.com/hashicorp/golang-lru/v2"
     	"github.com/octo-sts/app/pkg/maxsize"
    +	"github.com/octo-sts/app/pkg/oidcvalidate"
     )
     
     // MaximumResponseSize is the maximum size of allowed responses from
    @@ -45,6 +46,13 @@ func Get(ctx context.Context, issuer string) (provider VerifierProvider, err err
     
     	ctx = oidc.ClientContext(ctx, &http.Client{
     		Transport: maxsize.NewRoundTripper(MaximumResponseSize, httpmetrics.Transport),
    +		CheckRedirect: func(req *http.Request, _ []*http.Request) error {
    +			// Validate redirect destination using same rules as original issuer
    +			if !oidcvalidate.IsValidIssuer(req.URL.String()) {
    +				return fmt.Errorf("redirect destination %q failed issuer validation", req.URL.String())
    +			}
    +			return nil
    +		},
     	})
     
     	// Verify the token before we trust anything about it.
    

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

5

News mentions

0

No linked articles in our index yet.