VYPR
Critical severity9.4GHSA Advisory· Published May 11, 2026· Updated May 13, 2026

CVE-2026-42882

CVE-2026-42882

Description

oxyno-zeta/s3-proxy is an aws s3 proxy written in go. Prior to 5.0.0, s3-proxy contains an authentication bypass caused by inconsistent URL path interpretation between the authentication middleware and the bucket handler. The authentication middleware evaluates resource path patterns against the percent-encoded request URI (r.URL.RequestURI()), while the bucket handler constructs S3 object keys from the decoded path (r.URL.Path). This mismatch, combined with the glob library being invoked without a path separator (causing * to match across / boundaries), allows unauthenticated attackers to write to, read from, or delete objects in protected S3 namespaces. Exploitation is possible via three techniques: (1) using * patterns that match across path separators to reach protected routes via path traversal (e.g., /open/foo/drafts/../restricted/), (2) using percent-encoded slashes (%2F) to collapse multiple path segments into a single token at the auth layer while the decoded form resolves to a protected namespace at the storage layer, and (3) using dot-dot segments (../) under ** prefix patterns, where the raw path matches an open route while Go's URL parser resolves the traversal to a protected path before the bucket handler runs. An unauthenticated attacker with network access can perform unauthorized PUT, GET, or DELETE operations on objects in authentication-protected S3 namespaces. This vulnerability is fixed in 5.0.0.

Affected products

1

Patches

2
af5ff57d8c60

fix: reject dot-segment path traversals before routing and auth

https://github.com/oxyno-zeta/s3-proxyargos83Apr 14, 2026via ghsa
4 files changed · +401 0
  • pkg/s3-proxy/server/middlewares/reject-traversal.go+69 0 added
    @@ -0,0 +1,69 @@
    +package middlewares
    +
    +import (
    +	"net/http"
    +	"net/url"
    +	"path"
    +	"strings"
    +
    +	"emperror.dev/errors"
    +
    +	"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
    +	responsehandler "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/response-handler"
    +)
    +
    +// RejectTraversal returns a middleware that rejects requests whose URL path
    +// contains dot-segment traversals before routing and auth run. It fully decodes
    +// the escaped path (including %2F and %2E) and compares the non-empty segment
    +// count before and after path.Clean. A change in count means a . or .. segment
    +// was present — either literal or encoded — and the request is rejected with
    +// 400 Bad Request.
    +//
    +// Consecutive slashes (//) do not change the non-empty segment count and are
    +// allowed through (the router normalises them).
    +func RejectTraversal(cfgManager config.Manager) func(http.Handler) http.Handler {
    +	return func(next http.Handler) http.Handler {
    +		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +			hasDot, err := isPathTraversalAttempt(r.URL.EscapedPath())
    +			if err != nil {
    +				responsehandler.GeneralBadRequestError(r, w, cfgManager, errors.WithStack(err))
    +
    +				return
    +			}
    +
    +			if hasDot {
    +				responsehandler.GeneralBadRequestError(
    +					r,
    +					w,
    +					cfgManager,
    +					errors.New("path traversal detected"),
    +				)
    +
    +				return
    +			}
    +
    +			next.ServeHTTP(w, r)
    +		})
    +	}
    +}
    +
    +func isPathTraversalAttempt(escapedPath string) (bool, error) {
    +	decoded, err := url.PathUnescape(escapedPath)
    +	if err != nil {
    +		return false, err
    +	}
    +
    +	return countSegments(decoded) != countSegments(path.Clean(decoded)), nil
    +}
    +
    +func countSegments(rawPath string) int {
    +	count := 0
    +
    +	for segment := range strings.SplitSeq(rawPath, "/") {
    +		if segment != "" {
    +			count++
    +		}
    +	}
    +
    +	return count
    +}
    
  • pkg/s3-proxy/server/middlewares/reject-traversal_test.go+152 0 added
    @@ -0,0 +1,152 @@
    +//go:build unit
    +
    +package middlewares
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestIsPathTraversalAttempt(t *testing.T) {
    +	tests := []struct {
    +		name        string
    +		escapedPath string
    +		want        bool
    +		wantErr     bool
    +	}{
    +		{
    +			name:        ".. traversal is detected",
    +			escapedPath: "/public/../secret/traversal.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        ".. at root is detected",
    +			escapedPath: "/../secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "single . segment is detected",
    +			escapedPath: "/public/./file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "%2E%2E traversal is detected",
    +			escapedPath: "/public/%2E%2E/secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "lowercase %2e%2e traversal is detected",
    +			escapedPath: "/public/%2e%2e/secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "mixed-case %2E%2e traversal is detected",
    +			escapedPath: "/public/%2E%2e/secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "%2E%2E at root is detected",
    +			escapedPath: "/%2E%2E/secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "single %2E segment is detected",
    +			escapedPath: "/public/%2E/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "mixed .%2E traversal is detected",
    +			escapedPath: "/public/.%2E/secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "mixed %2E. traversal is detected",
    +			escapedPath: "/public/%2E./secret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "%2F-based double traversal is detected",
    +			escapedPath: "/open/value%2F..%2F..%2Frestricted/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "%2F-based single traversal is detected",
    +			escapedPath: "/public/foo%2F..%2Fsecret/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "%2F-based traversal mixed with legitimate %2F is detected",
    +			escapedPath: "/upload/foo%2F..%2Fbar%2Fbaz/file.txt",
    +			want:        true,
    +		},
    +		{
    +			name:        "invalid percent-encoding returns error",
    +			escapedPath: "/public/%zz/file.txt",
    +			wantErr:     true,
    +		},
    +		{
    +			name:        "consecutive slashes are allowed (accidental double-slash)",
    +			escapedPath: "/public//file.txt",
    +			want:        false,
    +		},
    +		{
    +			name:        ".foo filename is allowed",
    +			escapedPath: "/dir/.foo/",
    +			want:        false,
    +		},
    +		{
    +			name:        "..foo filename is allowed",
    +			escapedPath: "/dir/..foo/",
    +			want:        false,
    +		},
    +		{
    +			name:        "dir.with..dots is allowed",
    +			escapedPath: "/dir.with..dots/",
    +			want:        false,
    +		},
    +		{
    +			name:        "%2Efoo encoded filename is allowed",
    +			escapedPath: "/dir/%2Efoo",
    +			want:        false,
    +		},
    +		{
    +			name:        "%2E%2Efoo encoded filename is allowed",
    +			escapedPath: "/dir/%2E%2Efoo",
    +			want:        false,
    +		},
    +		{
    +			name:        "%2F encoded slash in resource name is allowed",
    +			escapedPath: "/upload/foo%2Fbar/file.txt",
    +			want:        false,
    +		},
    +		{
    +			name:        "%20 encoded space is allowed",
    +			escapedPath: "/public/my%20file.txt",
    +			want:        false,
    +		},
    +		{
    +			name:        "normal path is allowed",
    +			escapedPath: "/public/normal.txt",
    +			want:        false,
    +		},
    +		{
    +			name:        "trailing slash is allowed",
    +			escapedPath: "/public/dir/",
    +			want:        false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := isPathTraversalAttempt(tt.escapedPath)
    +
    +			if tt.wantErr {
    +				assert.Error(t, err)
    +			} else {
    +				assert.NoError(t, err)
    +				assert.Equal(t, tt.want, got)
    +			}
    +		})
    +	}
    +}
    
  • pkg/s3-proxy/server/server.go+3 0 modified
    @@ -185,6 +185,9 @@ func (svr *Server) generateRouter() (http.Handler, error) {
     		r.Use(cc.Handler)
     	}
     
    +	// Reject path traversal (dot-segments) before routing and auth.
    +	r.Use(middlewares.RejectTraversal(svr.cfgManager))
    +
     	// Check if auth if enabled and oidc enabled
     	if cfg.AuthProviders != nil && cfg.AuthProviders.OIDC != nil {
     		for k, v := range cfg.AuthProviders.OIDC {
    
  • pkg/s3-proxy/server/server_integration_test.go+177 0 modified
    @@ -7457,6 +7457,183 @@ func TestGlobSeparatorSingleStar(t *testing.T) {
     	}
     }
     
    +func TestPathTraversalPrevention(t *testing.T) {
    +	trueValue := true
    +
    +	accessKey := "YOUR-ACCESSKEYID"
    +	secretAccessKey := "YOUR-SECRETACCESSKEY"
    +	region := "eu-central-1"
    +	bucket := "test-bucket"
    +	_, s3server, err := setupFakeS3(accessKey, secretAccessKey, region, bucket)
    +	defer s3server.Close()
    +	if err != nil {
    +		t.Error(err)
    +		return
    +	}
    +
    +	cfg := &config.Config{
    +		Server: &config.ServerConfig{
    +			Compress: &config.ServerCompressConfig{
    +				Enabled: &config.DefaultServerCompressEnabled,
    +				Level:   config.DefaultServerCompressLevel,
    +				Types:   config.DefaultServerCompressTypes,
    +			},
    +		},
    +		ListTargets: &config.ListTargetsConfig{},
    +		Tracing:     &config.TracingConfig{},
    +		Templates:   testsDefaultGeneralTemplateConfig,
    +		AuthProviders: &config.AuthProviderConfig{
    +			Basic: map[string]*config.BasicAuthConfig{
    +				"provider1": {Realm: "realm1"},
    +			},
    +		},
    +		Targets: map[string]*config.TargetConfig{
    +			"target1": {
    +				Name: "target1",
    +				Bucket: &config.BucketConfig{
    +					Name:       bucket,
    +					Region:     region,
    +					S3Endpoint: s3server.URL,
    +					Credentials: &config.BucketCredentialConfig{
    +						AccessKey: &config.CredentialConfig{Value: accessKey},
    +						SecretKey: &config.CredentialConfig{Value: secretAccessKey},
    +					},
    +					DisableSSL: true,
    +				},
    +				Mount: &config.MountConfig{Path: []string{"/"}},
    +				Resources: []*config.Resource{
    +					{
    +						Path:     "/secret/**",
    +						Methods:  []string{"GET"},
    +						Provider: "provider1",
    +						Basic: &config.ResourceBasic{
    +							Credentials: []*config.BasicAuthUserConfig{
    +								{User: "user1", Password: &config.CredentialConfig{Value: "pass1"}},
    +							},
    +						},
    +					},
    +					{
    +						Path:      "/public/**",
    +						Methods:   []string{"GET"},
    +						WhiteList: &trueValue,
    +					},
    +					{
    +						Path:     "/restricted/*/files",
    +						Methods:  []string{"GET"},
    +						Provider: "provider1",
    +						Basic: &config.ResourceBasic{
    +							Credentials: []*config.BasicAuthUserConfig{
    +								{User: "user1", Password: &config.CredentialConfig{Value: "pass1"}},
    +							},
    +						},
    +					},
    +					{
    +						Path:      "/open/*/files",
    +						Methods:   []string{"GET"},
    +						WhiteList: &trueValue,
    +					},
    +				},
    +				Actions: &config.ActionsConfig{
    +					GET: &config.GetActionConfig{Enabled: true},
    +				},
    +			},
    +		},
    +	}
    +
    +	ctrl := gomock.NewController(t)
    +	cfgManagerMock := cmocks.NewMockManager(ctrl)
    +	cfgManagerMock.EXPECT().GetConfig().AnyTimes().Return(cfg)
    +
    +	logger := log.NewLogger()
    +	tsvc, err := tracing.New(cfgManagerMock, logger)
    +	assert.NoError(t, err)
    +
    +	s3Manager := s3client.NewManager(cfgManagerMock, metricsCtx)
    +	err = s3Manager.Load()
    +	assert.NoError(t, err)
    +
    +	webhookManager := webhook.NewManager(cfgManagerMock, metricsCtx)
    +
    +	svr := &Server{
    +		logger:          logger,
    +		cfgManager:      cfgManagerMock,
    +		metricsCl:       metricsCtx,
    +		tracingSvc:      tsvc,
    +		s3clientManager: s3Manager,
    +		webhookManager:  webhookManager,
    +	}
    +
    +	router, err := svr.generateRouter()
    +	assert.NoError(t, err)
    +
    +	tests := []struct {
    +		name         string
    +		url          string
    +		expectedCode int
    +	}{
    +		{
    +			name:         "%2E%2E traversal is rejected before auth",
    +			url:          "http://localhost/public/%2E%2E/secret/traversal.txt",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         ".. traversal is rejected before auth",
    +			url:          "http://localhost/public/../secret/traversal.txt",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         ".. traversal from /open/*/files is rejected before auth",
    +			url:          "http://localhost/open/../restricted/foo/files",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         "%2E%2E traversal that escapes all routes is rejected before auth",
    +			url:          "http://localhost/open/%2E%2E/files",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         ".. traversal that escapes all routes is rejected before auth",
    +			url:          "http://localhost/open/../files",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         "%2F-based traversal from /public/** is rejected before auth",
    +			url:          "http://localhost/public/foo%2F..%2F..%2Fsecret/traversal.txt",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         "%2F-based traversal from /open/*/files is rejected before auth",
    +			url:          "http://localhost/open/foo%2F..%2F..%2Frestricted%2Fbar/files",
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		{
    +			name:         "normal path on /public/** whitelisted resource passes auth",
    +			url:          "http://localhost/public/normal.txt",
    +			expectedCode: http.StatusNotFound,
    +		},
    +		{
    +			name:         "normal path on /restricted/*/files requires auth",
    +			url:          "http://localhost/restricted/foo/files",
    +			expectedCode: http.StatusUnauthorized,
    +		},
    +		{
    +			name:         "normal path on /open/*/files whitelisted resource passes auth",
    +			url:          "http://localhost/open/foo/files",
    +			expectedCode: http.StatusNotFound,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			req, err := http.NewRequest(http.MethodGet, tt.url, nil)
    +			assert.NoError(t, err)
    +			w := httptest.NewRecorder()
    +			router.ServeHTTP(w, req)
    +			assert.Equal(t, tt.expectedCode, w.Code)
    +		})
    +	}
    +}
    +
     func TestTrailingSlashRedirect(t *testing.T) {
     	accessKey := "YOUR-ACCESSKEYID"
     	secretAccessKey := "YOUR-SECRETACCESSKEY"
    
1320e4abd46a

fix!: add '/' separator to glob pattern compilation for resource path matching

https://github.com/oxyno-zeta/s3-proxyargos83Apr 14, 2026via ghsa
6 files changed · +285 23
  • docs/configuration/example.md+12 12 modified
    @@ -291,14 +291,14 @@ targets:
           # A specific host can be added for filtering. Otherwise, all hosts will be accepted
           # host: localhost:8080
         # ## Resources declaration
    -    # ## WARNING: Think about all path that you want to protect. At the end of the list, you should add a resource filter for /* otherwise, it will be public.
    +    # ## WARNING: Think about all path that you want to protect. At the end of the list, you should add a resource filter for /** otherwise, it will be public.
         # resources:
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
         #   - path: /
         #     # Whitelist
         #     whiteList: true
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    -    #   - path: /specific_doc/*
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
    +    #   - path: /specific_doc/**
         #     # HTTP Methods authorized (Must be in HEAD, GET, PUT or DELETE)
         #     methods:
         #       - GET
    @@ -311,8 +311,8 @@ targets:
         #       # NOTE: This list can be empty ([]) for authentication only and no group filter
         #       authorizationAccesses: # Authorization accesses : groups or email or regexp
         #         - group: specific_users
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    -    #   - path: /directory1/*
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
    +    #   - path: /directory1/**
         #     # HTTP Methods authorized (Must be in HEAD, GET, PUT or DELETE)
         #     methods:
         #       - GET
    @@ -326,16 +326,16 @@ targets:
         #         - user: user1
         #           password:
         #             path: password1-in-file
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    -    #   - path: /opa-protected/*
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
    +    #   - path: /opa-protected/**
         #     # OIDC section for access filter
         #     oidc:
         #       # Authorization through OPA server configuration
         #       authorizationOPAServer:
         #         # OPA server url with data path
         #         url: http://localhost:8181/v1/data/example/authz/allowed
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    -    #   - path: /specific_doc/*
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
    +    #   - path: /specific_doc/**
         #     # HTTP Methods authorized (Must be in HEAD, GET, PUT or DELETE)
         #     methods:
         #       - GET
    @@ -348,8 +348,8 @@ targets:
         #       # NOTE: This list can be empty ([]) for authentication only and no group filter
         #       authorizationAccesses: # Authorization accesses : groups or email or regexp
         #         - group: specific_users
    -    #   # A Path must be declared for a resource filtering (a wildcard can be added to match every sub path)
    -    #   - path: /opa-protected/*
    +    #   # A Path must be declared for a resource filtering (* matches one path segment, ** matches across path boundaries)
    +    #   - path: /opa-protected/**
         #     # Header section for access filter
         #     header:
         #       # Authorization through OPA server configuration
    
  • docs/configuration/structure.md+1 1 modified
    @@ -416,7 +416,7 @@ This authentication method should be used only with a software like [Oauth2-prox
     
     | Key       | Type                                      | Required                                    | Default | Description                                                          |
     | --------- | ----------------------------------------- | ------------------------------------------- | ------- | -------------------------------------------------------------------- |
    -| path      | String                                    | Yes                                         | None    | Path or matching path (e.g.: `/*`)                                   |
    +| path      | String                                    | Yes                                         | None    | Path or glob pattern for resource matching. `*` matches exactly one path segment (e.g. `/folder/*` matches `/folder/file.txt` but not `/folder/sub/file.txt`). `**` matches across path boundaries (e.g. `/folder/**` matches any path under `/folder/`). Use `/**` as a catch-all for all paths. |
     | provider  | String                                    | Yes                                         | None    | Provider key reference                                               |
     | methods   | [String]                                  | No                                          | `[GET]` | HTTP methods allowed (Allowed values `HEAD`, `GET`, `PUT`, `DELETE`) |
     | whiteList | Boolean                                   | Required without oidc or basic              | None    | Is this path in white list ? E.g.: No authentication                 |
    
  • docs/feature-guide/authorization-accesses.md+6 6 modified
    @@ -18,7 +18,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses: []
    @@ -46,7 +46,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses:
    @@ -76,7 +76,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses:
    @@ -106,7 +106,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses:
    @@ -135,7 +135,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses:
    @@ -171,7 +171,7 @@ Example of authorization accesses configuration:
     targets:
       target1:
         resources:
    -      - path: /*
    +      - path: /**
             provider: provider1
             oidc:
               authorizationAccesses:
    
  • pkg/s3-proxy/authx/authentication/main.go+1 1 modified
    @@ -144,7 +144,7 @@ func findResource(resL []*config.Resource, requestURI, httpMethod string) (*conf
     			continue
     		}
     		// Compile a glob pattern for uri matching
    -		g, err := glob.Compile(res.Path)
    +		g, err := glob.Compile(res.Path, '/')
     		// Check if error exists
     		if err != nil {
     			return nil, errors.WithStack(err)
    
  • pkg/s3-proxy/authx/authentication/main_test.go+138 3 modified
    @@ -50,15 +50,15 @@ func Test_findResource(t *testing.T) {
     			wantErr: false,
     		},
     		{
    -			name: "Should find a valid resource with glob path",
    +			name: "Wildcard * at end matches a single segment",
     			args: args{
     				resL: []*config.Resource{
     					{
     						Path:    "/test/*",
     						Methods: []string{"GET"},
     					},
     				},
    -				requestURI: "/test/fake/fake",
    +				requestURI: "/test/fake",
     				httpMethod: "GET",
     			},
     			want: &config.Resource{
    @@ -67,6 +67,141 @@ func Test_findResource(t *testing.T) {
     			},
     			wantErr: false,
     		},
    +		{
    +			name: "Wildcard * at end does not match multiple segments",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/test/*",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/test/fake/fake",
    +				httpMethod: "GET",
    +			},
    +			want:    nil,
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard * between segments matches exactly one segment",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/single/*/segment/",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/single/foo/segment/",
    +				httpMethod: "GET",
    +			},
    +			want: &config.Resource{
    +				Path:    "/single/*/segment/",
    +				Methods: []string{"GET"},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard * between segments does not match multiple segments",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/single/*/segment/",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/single/foo/bar/segment/",
    +				httpMethod: "GET",
    +			},
    +			want:    nil,
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard ** at end matches a single segment",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/test/**",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/test/fake",
    +				httpMethod: "GET",
    +			},
    +			want: &config.Resource{
    +				Path:    "/test/**",
    +				Methods: []string{"GET"},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard ** at end matches multiple segments",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/test/**",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/test/fake/fake",
    +				httpMethod: "GET",
    +			},
    +			want: &config.Resource{
    +				Path:    "/test/**",
    +				Methods: []string{"GET"},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard ** between segments matches a single intermediate segment",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/multiple/**/segments/",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/multiple/foo/segments/",
    +				httpMethod: "GET",
    +			},
    +			want: &config.Resource{
    +				Path:    "/multiple/**/segments/",
    +				Methods: []string{"GET"},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard ** between segments matches multiple intermediate segments",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/multiple/**/segments/",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/multiple/foo/bar/segments/",
    +				httpMethod: "GET",
    +			},
    +			want: &config.Resource{
    +				Path:    "/multiple/**/segments/",
    +				Methods: []string{"GET"},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name: "Wildcard ** between segments does not match when suffix differs",
    +			args: args{
    +				resL: []*config.Resource{
    +					{
    +						Path:    "/multiple/**/segments/",
    +						Methods: []string{"GET"},
    +					},
    +				},
    +				requestURI: "/multiple/foo/other/",
    +				httpMethod: "GET",
    +			},
    +			want:    nil,
    +			wantErr: false,
    +		},
     		{
     			name: "Shouldn't find a valid resource with valid glob path but not http method",
     			args: args{
    @@ -76,7 +211,7 @@ func Test_findResource(t *testing.T) {
     						Methods: []string{"GET", "PUT"},
     					},
     				},
    -				requestURI: "/test/fake/fake",
    +				requestURI: "/test/fake",
     				httpMethod: "POST",
     			},
     			want:    nil,
    
  • pkg/s3-proxy/server/server_integration_test.go+127 0 modified
    @@ -7330,6 +7330,133 @@ func TestFolderWithSubFolders(t *testing.T) {
     
     }
     
    +func TestGlobSeparatorSingleStar(t *testing.T) {
    +	trueValue := true
    +
    +	accessKey := "YOUR-ACCESSKEYID"
    +	secretAccessKey := "YOUR-SECRETACCESSKEY"
    +	region := "eu-central-1"
    +	bucket := "test-bucket"
    +	_, s3server, err := setupFakeS3(accessKey, secretAccessKey, region, bucket)
    +	defer s3server.Close()
    +	if err != nil {
    +		t.Error(err)
    +		return
    +	}
    +
    +	cfg := &config.Config{
    +		Server: &config.ServerConfig{
    +			Compress: &config.ServerCompressConfig{
    +				Enabled: &config.DefaultServerCompressEnabled,
    +				Level:   config.DefaultServerCompressLevel,
    +				Types:   config.DefaultServerCompressTypes,
    +			},
    +		},
    +		ListTargets: &config.ListTargetsConfig{},
    +		Tracing:     &config.TracingConfig{},
    +		Templates:   testsDefaultGeneralTemplateConfig,
    +		AuthProviders: &config.AuthProviderConfig{
    +			Basic: map[string]*config.BasicAuthConfig{
    +				"provider1": {Realm: "realm1"},
    +			},
    +		},
    +		Targets: map[string]*config.TargetConfig{
    +			"target1": {
    +				Name: "target1",
    +				Bucket: &config.BucketConfig{
    +					Name:       bucket,
    +					Region:     region,
    +					S3Endpoint: s3server.URL,
    +					Credentials: &config.BucketCredentialConfig{
    +						AccessKey: &config.CredentialConfig{Value: accessKey},
    +						SecretKey: &config.CredentialConfig{Value: secretAccessKey},
    +					},
    +					DisableSSL: true,
    +				},
    +				Mount: &config.MountConfig{Path: []string{"/"}},
    +				Resources: []*config.Resource{
    +					{
    +						Path:     "/upload/*/restricted",
    +						Methods:  []string{"DELETE"},
    +						Provider: "provider1",
    +						Basic: &config.ResourceBasic{
    +							Credentials: []*config.BasicAuthUserConfig{
    +								{User: "user1", Password: &config.CredentialConfig{Value: "pass1"}},
    +							},
    +						},
    +					},
    +					{
    +						Path:      "/upload/*/open",
    +						Methods:   []string{"DELETE"},
    +						WhiteList: &trueValue,
    +					},
    +				},
    +				Actions: &config.ActionsConfig{
    +					DELETE: &config.DeleteActionConfig{Enabled: true},
    +				},
    +			},
    +		},
    +	}
    +
    +	ctrl := gomock.NewController(t)
    +	cfgManagerMock := cmocks.NewMockManager(ctrl)
    +	cfgManagerMock.EXPECT().GetConfig().AnyTimes().Return(cfg)
    +
    +	logger := log.NewLogger()
    +	tsvc, err := tracing.New(cfgManagerMock, logger)
    +	assert.NoError(t, err)
    +
    +	s3Manager := s3client.NewManager(cfgManagerMock, metricsCtx)
    +	err = s3Manager.Load()
    +	assert.NoError(t, err)
    +
    +	webhookManager := webhook.NewManager(cfgManagerMock, metricsCtx)
    +
    +	svr := &Server{
    +		logger:          logger,
    +		cfgManager:      cfgManagerMock,
    +		metricsCl:       metricsCtx,
    +		tracingSvc:      tsvc,
    +		s3clientManager: s3Manager,
    +		webhookManager:  webhookManager,
    +	}
    +
    +	router, err := svr.generateRouter()
    +	assert.NoError(t, err)
    +
    +	tests := []struct {
    +		name         string
    +		url          string
    +		expectedCode int
    +	}{
    +		{
    +			name:         "single-segment * matches single path segment",
    +			url:          "http://localhost/upload/foo/open",
    +			expectedCode: http.StatusNoContent,
    +		},
    +		{
    +			name:         "single-segment * matches single path segment on protected route",
    +			url:          "http://localhost/upload/foo/restricted",
    +			expectedCode: http.StatusUnauthorized,
    +		},
    +		{
    +			name:         "* does not cross path separator — multi-segment produces 403",
    +			url:          "http://localhost/upload/foo/bar/open",
    +			expectedCode: http.StatusForbidden,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			req, err := http.NewRequest(http.MethodDelete, tt.url, nil)
    +			assert.NoError(t, err)
    +			w := httptest.NewRecorder()
    +			router.ServeHTTP(w, req)
    +			assert.Equal(t, tt.expectedCode, w.Code)
    +		})
    +	}
    +}
    +
     func TestTrailingSlashRedirect(t *testing.T) {
     	accessKey := "YOUR-ACCESSKEYID"
     	secretAccessKey := "YOUR-SECRETACCESSKEY"
    

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.