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- Range: < 0.0.0-20260424211602-1320e4abd46a
Patches
2af5ff57d8c60fix: reject dot-segment path traversals before routing and auth
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"
1320e4abd46afix!: add '/' separator to glob pattern compilation for resource path matching
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- github.com/advisories/GHSA-rfgq-wgg8-662pghsaADVISORY
- github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51nvd
- github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48anvd
- github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662pnvd
- nvd.nist.gov/vuln/detail/CVE-2026-42882ghsa
News mentions
0No linked articles in our index yet.