{"id":"CVE-2026-42882","revision":null,"records":[{"revision":"71c5aa1331fd4fffbf12c52863942d1b","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-42882","aliases":[],"title":"oxyno-zeta/s3-proxy: Security Issues in Resource Path Matching","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\nthat 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.","state":0,"publishedAt":"2026-05-11T19:26:44.897+00:00","modifiedAt":"2026-05-13T14:40:36.658+00:00","affected":[{"ecosystem":"Vendor product","name":"s3-proxy","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"oxyno-zeta\",\n                    \"product\": \"s3-proxy\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 5.0.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L","score":9.4,"severity":"CRITICAL"}],"references":["https://github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662p","https://github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51","https://github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48a"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-42882","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-42882","identifiers":["CVE-2026-42882"]},"retained":false,"historicalAffected":[]},{"revision":"d2346218ea5e42988c2d225357426163","observedAt":"2026-11-07T09:36:13.617+00:00","statement":{"source":"euvd","id":"EUVD-2026-29205","aliases":["CVE-2026-42882","GHSA-rfgq-wgg8-662p"],"title":"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…","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\nthat 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.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L","score":9.4,"severity":null}],"references":["https://github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662p","https://github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51","https://github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48a"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-29205","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"May 11, 2026, 7:26:44 PM","dateUpdated":"May 13, 2026, 2:40:36 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-29205","identifiers":["EUVD-2026-29205","CVE-2026-42882","GHSA-rfgq-wgg8-662p"]},"retained":false,"historicalAffected":[]},{"revision":"214a08afffc34983be100731dd263bc2","observedAt":"2026-10-27T11:12:04.713+00:00","statement":{"source":"nvd","id":"CVE-2026-42882","aliases":[],"title":"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…","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\nthat 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.","state":0,"publishedAt":"2026-05-11T20:25:44.45+00:00","modifiedAt":"2026-10-26T18:10:00.173+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L","score":9.4,"severity":"CRITICAL"}],"references":["https://github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51","https://github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48a","https://github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662p"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-42882","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-22","CWE-863"],"productCriteria":[],"sourceDates":{"published":"2026-05-11T20:25:44.450","lastModified":"2026-10-26T18:10:00.173"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-42882","identifiers":["CVE-2026-42882"]},"retained":false,"historicalAffected":[]},{"revision":"f9ac11eee02043f382bb83c3bff951e9","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GHSA-rfgq-wgg8-662p","aliases":["CVE-2026-42882","GO-2026-5616"],"title":"S3-Proxy has Security Issues in its Resource Path Matching Implementation","description":"## Background\n\nThe original concern is functional: a resource pattern should treat a percent-encoded segment like some%2Fvalue as a single opaque token rather than splitting it into two path segments at the decoded /. Investigation into why %2F was being decoded and how routes matched against the result surfaced three related security issues, documented below.\n\nRather than landing a fix directly, the problem space warrants discussion first. Different fixes carry different compliance and compatibility tradeoffs, and every viable option is a breaking change in some form. Aligning on a direction before committing to an implementation is the safer path.\n\n## Root cause: two different path representations\n\nGo's `net/http` decodes percent-encoded characters when it parses an incoming URL:\n`%2F` becomes `/` in `r.URL.Path`, while the original encoded form is preserved in\n`r.URL.RawPath`. Two different parts of s3-proxy use different fields:\n\n- The **auth middleware** calls `r.URL.RequestURI()`, which returns the encoded\n  form (from `RawPath` when available). It sees `%2F` as literal characters, not\n  as path separators.\n- The **bucket handler** reads `r.URL.Path` to build the S3 key. It sees the\n  decoded form, where `%2F` has already become `/`.\n\nAll three issues stem from this mismatch, combined with how glob patterns are\ncompiled. The examples below use PUT for concreteness, but the auth bypass applies\nto any HTTP method — a config that restricts GET or DELETE on a namespace is\nequally affected, meaning an attacker could read from or delete objects in a\nprotected namespace without credentials.\n\n### A note on RFC 3986\n\nRFC 3986 §2.2 states that `/` and `%2F` are **not equivalent** in a URI path:\n\n> URIs that differ in the replacement of a reserved character with its\n> corresponding percent-encoded octet are **not** equivalent.\n\n`/` is a reserved gen-delim used as a path segment separator. `%2F` is its\npercent-encoded form and, by the RFC, should be treated as data *within* a\nsegment — not as a separator. So:\n\n- `/foo/bar/baz` → three segments: `foo`, `bar`, `baz`\n- `/foo%2Fbar/baz` → two segments: `foo/bar` (opaque data), `baz`\n\nThe original functional concern (wanting `foo%2Fbar` to match as a single token\nagainst a single-segment wildcard) is therefore RFC-correct behaviour. Go's\n`r.URL.Path` violates this by decoding `%2F` to `/`, collapsing the two\nrepresentations into one. This is the underlying tension that makes fixing these\nissues non-trivial: the simplest security fix makes s3-proxy *more* RFC\nnon-compliant, while the RFC-correct fix requires a more significant refactor.\n\n### A note on breaking changes\n\nAny of the proposed fixes for these issues should be treated as a **breaking\nchange**. Each option alters how path patterns in existing configs are interpreted\n— whether by changing how `*` matches segments, by shifting which path\nrepresentation auth matches against, or by normalising paths before they reach the\nrouter. Operators upgrading to a fixed version will need to review their resource\npath definitions, and a clear migration note in the changelog is essential\nregardless of which approach is chosen.\n\nOne way to avoid a hard breaking change would be to introduce a new field — for\nexample `route:` — that carries the fixed semantics, while keeping the existing\n`path:` field with its current behaviour (and marking it deprecated). Operators\ncould migrate resource definitions incrementally, and the security fix would be\navailable immediately without requiring a coordinated config update across all\ndeployments. The obvious cost of this approach is maintaining two parallel\nimplementations, duplicated test coverage, and the ongoing burden of supporting\na deprecated code path until it can eventually be removed.\n\n---\n\n## Issue 1 — `*` in resource paths matches across `/`\n\n### Background\n\nResource paths are matched using `github.com/gobwas/glob`. The call site is:\n\n```go\n// pkg/s3-proxy/authx/authentication/main.go\ng, err := glob.Compile(res.Path)\n```\n\n`glob.Compile` is called **without a separator argument**. Without a separator,\n`*` matches any character — including `/`. This means a pattern intended to protect\na single path segment actually matches across directory boundaries.\n\n### Example\n\nConsider a config with an open route and a protected route:\n\n```yaml\nresources:\n  # open — no auth required\n  - path: /upload/*/drafts/\n    methods: [PUT]\n    whiteList: true\n\n  # protected — basic auth required\n  - path: /upload/*/restricted/\n    methods: [PUT]\n    basic:\n      ...\n```\n\nThe intent is clear: `drafts` is open, `restricted` is protected. The `*` is meant\nto match a single path segment (the object identifier).\n\nHowever, because `*` crosses `/`, the pattern `/upload/*/drafts/` also matches:\n\n```\nPUT /upload/foo/drafts/../restricted/\n```\n\nThe path segment matched by `*` is `foo`, and then `drafts/../restricted/` is\nconsumed by the rest of the pattern — because without a separator, `*` is equivalent\nto `.*` and matches `/`, `.`, and everything else.\n\nThe result: an unauthenticated request is accepted by the open route.\n\n### Fix discussion\n\nThe straightforward fix is to pass `'/'` as the separator to `glob.Compile`:\n\n```go\n// before\ng, err := glob.Compile(res.Path)\n\n// after\ng, err := glob.Compile(res.Path, '/')\n```\n\nWith a separator set:\n- `*` matches any sequence of non-`/` characters (a single path segment).\n- `**` matches any sequence including `/` (crossing path boundaries).\n\nThis fix closes the Issue 1 attack above: with a separator, `drafts/../restricted/`\nis more than one segment and no longer matches the pattern `/upload/*/drafts/`.\n\n#### Breaking change\n\nAny existing config that relies on `*` crossing `/` must be updated to `**`. For\nexample:\n\n```yaml\n# before — worked accidentally because * crossed /\n- path: /upload/*/drafts/\n\n# after — single-segment match (behaviour unchanged for single-segment IDs)\n- path: /upload/*/drafts/\n\n# after — multi-segment match (e.g. nested object IDs containing /)\n- path: /upload/**/drafts/\n```\n\nA migration note in the changelog would be needed.\n\n---\n\n## Issue 2 — Percent-encoded slashes bypass auth via segment collapsing\n\n### Background\n\nWith Fix 1 applied, `*` only matches a single path segment. However, the auth\nmiddleware matches against `r.URL.RequestURI()` — the **encoded** path — while the\nbucket handler uses `r.URL.Path` — the **decoded** path. A client can use `%2F`\nto make what looks like a single segment in the encoded URI decode into multiple\nsegments including a protected path component.\n\n### Example\n\nUsing the same config as Issue 1:\n\n```\nPUT /upload/foo%2Frestricted/drafts/\n```\n\nStep by step:\n\n1. `r.URL.RawPath` = `/upload/foo%2Frestricted/drafts/`\n2. `r.URL.Path` (decoded) = `/upload/foo/restricted/drafts/`\n3. Auth middleware calls `r.URL.RequestURI()` → returns the encoded form.\n4. With Fix 1's separator `/`, glob splits on the literal `/`. The segment between\n   the first and second slash is `foo%2Frestricted` — one token with no literal `/`\n   — so `*` matches it. Pattern `/upload/*/drafts/` fires.\n5. Open route → request proceeds without auth.\n6. Bucket handler uses `r.URL.Path` → S3 key is `upload/foo/restricted/drafts/…`\n   — **written into the restricted namespace without credentials**.\n\n### Proof via integration test\n\nI added `TestPercentEncodedSlashBypass` to\n`pkg/s3-proxy/server/server_integration_test.go`. The test sends a complete\nmultipart PUT without credentials and asserts a 401 response. It currently fails\nwith **204** — the file is written in full to the restricted namespace without any\nauthentication.\n\n### Fix discussion\n\nThis issue has two fundamentally different classes of fix, each with a different\nstance on RFC 3986 compliance.\n\n#### Option A — Match auth against the decoded path (`r.URL.Path`)\n\nChange the auth middleware to use `r.URL.Path` instead of `r.URL.RequestURI()`:\n\n```go\n// before\nrequestURI := r.URL.RequestURI()\n\n// after\nrequestURI := r.URL.Path\n```\n\nBoth auth and the bucket handler now operate on the same decoded string, closing\nthe mismatch that enables the bypass.\n\n**Pros:** One-line change; no other code touched; closes the bypass completely.\n\n**Cons:** RFC 3986 non-compliant — `/foo%2Fbar/baz` and `/foo/bar/baz` become\nindistinguishable at the auth layer. A pattern like `/upload/*/drafts/` will match\nboth `PUT /upload/foo/drafts/` and `PUT /upload/foo%2F.../drafts/` identically\nafter decoding, making it impossible for operators to write a pattern that\ndistinguishes the two. Any path segment containing a literal `/` encoded as `%2F`\ncan never be matched as a single token by `*`.\n\n#### Option B — Use the raw path in both auth and key construction\n\nKeep `r.URL.RequestURI()` in the auth middleware (reverting the Option A change)\nand replace the bucket handler's decoded path extraction with `r.URL.EscapedPath()`\nstripped of the mount path prefix. The AWS SDK then handles percent-encoding the\nkey in the HTTP request to S3, with no manual segment splitting required.\n\nThis keeps `%2F` opaque at both layers: auth matches against the encoded form, and\nthe S3 key preserves the encoded characters verbatim.\n\n**Security mechanism:** the bypass attack (`PUT /upload/foo%2Frestricted/drafts/`)\nstill returns **204** — the open route genuinely matches, because\n`foo%2Frestricted` is one encoded segment and `*` accepts it. However, the key\nwritten to S3 is `upload/foo%2Frestricted/drafts/…` — a distinct namespace from\n`upload/foo/restricted/drafts/…`. The attacker cannot reach the protected prefix\nbecause `%2F` and `/` are treated as different characters all the way to storage.\n\n**AWS S3 compatibility confirmed:** S3 natively supports `%2F` in key names. A\nkey `upload/foo%2Fbar/file.txt` is stored and retrieved as a distinct object from\n`upload/foo/bar/file.txt`. All four operations (HEAD, GET, PUT, DELETE) work\ncorrectly with `%2F`-containing paths.\n\n**Pros:** RFC-compliant; `%2F` remains a meaningful encoding — `foo%2Fbar` is one\ntoken and `*` correctly matches it as a single segment; `/foo%2Fbar/baz` and\n`/foo/bar/baz` are distinct at both auth and storage layers; simpler than it\nsounds — no custom segment-splitting utility needed, just `r.URL.EscapedPath()` in\nthe handler.\nThe breaking change is **contained to config files, not clients**: the only clients that break\nare those relying on `*` crossing literal `/` — and those require a config change\nto `**` under any fix option. Clients that encode user input containing `/` as\n`%2F` in a path segment are preserved: `foo%2Fbar` is still one encoded segment,\nand `*` still matches it. Under Option A those same clients break — the decoded\nform splits into multiple segments that no longer match `*`. The required\nclient-side fix would be to filter or transform any `/` out of user input before\nbuilding the URL, which may not always be feasible if the `/` carries meaning.\n\n**Cons:** The auth middleware reverts to using the encoded path, which re-opens\nthe door to dot-segment bypass (Issue 3) if the path-cleaning middleware is not\nalso in place — the two fixes must be applied together.\n\nA note on the 204 response: a request like `PUT /upload/foo%2Frestricted/drafts/`\nreturns 204 under this option, which may look like a bypass at first glance. It is\nnot. If `%2F` carries meaning, `foo%2Frestricted` is a valid identifier\nindistinguishable from any other — the server has no basis to treat it as\nsuspicious. The correct security responsibility is to handle all inputs\nconsistently and safely, not to guess intent based on the content of user-provided\nvalues. The namespace separation guarantee satisfies that: whatever the client\nsends is handled the same way at both the auth and storage layers.\n\n#### Option C — Reject requests containing `%2F` in the path\n\nReturn 400 Bad Request for any request whose raw path contains `%2F`:\n\n```go\nif strings.Contains(r.URL.RawPath, \"%2F\") || strings.Contains(r.URL.RawPath, \"%2f\") {\n    http.Error(w, \"Bad Request\", http.StatusBadRequest)\n    return\n}\n```\n\n**Pros:** Simplest possible enforcement; eliminates the ambiguity entirely.\n\n**Cons:** Breaks any client that sends object names containing `/` encoded as\n`%2F`; rules out a legitimate and RFC-sanctioned use of percent-encoding.\n\n---\n\n## Issue 3 — Dot-dot segments bypass authentication with prefix patterns\n\n### Background\n\nIssues 1 and 2 both involve `*` (single-segment wildcard). A different class of\nbypass survives Fix 1 and Fix 2 when configs use prefix-style patterns with `**`\nat the end, such as `/open/**`. This is a natural and common pattern for \"allow\neverything under this prefix.\" The `**` token is explicitly designed to cross `/`,\nso `..` traversal within that prefix still reaches protected paths.\n\nNote that `%2F..%2F` encoded traversal is a variant of this issue: the decoded\nform (`/../`) contains dot segments that `**` can consume, as described in the\nroot cause section.\n\n### Example\n\nConsider this config:\n\n```yaml\nresources:\n  # protected — basic auth required for anything under /restricted/\n  - path: /restricted/**\n    methods: [PUT]\n    basic:\n      ...\n\n  # open — no auth required for anything under /open/\n  - path: /open/**\n    methods: [PUT]\n    whiteList: true\n```\n\nWithout any path normalization, the following request bypasses auth:\n\n```\nPUT /open/../restricted/secret.json\n```\n\nStep by step:\n\n1. Go's `net/url` resolves dot segments when parsing the request URI: `r.URL.Path`\n   is `/restricted/secret.json`. The raw form `../` is preserved only in\n   `r.URL.RawPath`.\n2. The auth middleware calls `r.URL.RequestURI()`, which returns the encoded\n   form — `/open/../restricted/secret.json` — and evaluates resources against that.\n3. `/restricted/**` does not match because the raw path does not start with `/restricted/`.\n4. `/open/**` matches: `**` is allowed to cross `/`, so it consumes `../restricted/secret.json`.\n5. The open route fires — no auth required — the request returns 204.\n6. The bucket handler reads `r.URL.Path` — already `/restricted/secret.json` — and\n   writes the file directly into the restricted namespace.\n\n**Confirmed against AWS S3**: the file lands at `restricted/secret.json` — not at\na key containing `../`. Go resolves the dot segments before the bucket handler runs,\nso the write goes straight into the protected prefix. This makes the attack more\nsevere than a key-naming anomaly: it is a direct, confirmed write into the\nrestricted namespace with no authentication.\n\n### Proof via integration test\n\nI added `TestPathTraversalDoubleStarPrefix` to\n`pkg/s3-proxy/server/server_integration_test.go`. It uses the exact config above\nand shows that, with a path-cleaning middleware applied **before** the auth\nmiddleware, the traversal returns 401 instead of 204:\n\n```go\n{\n    // /open/** still matches /open/../restricted/file because ** crosses '/'.\n    // cleanPathMiddleware resolves the path to /restricted/file first, which\n    // matches the protected resource -> 401.\n    // Without cleanPathMiddleware this would return 204 (auth bypassed).\n    name:         \"traversal from open to restricted via ** prefix pattern is blocked\",\n    inputMethod:  \"PUT\",\n    inputURL:     \"http://localhost/open/../restricted/file.txt\",\n    expectedCode: 401,\n},\n```\n\n### Note on `%2E` (percent-encoded dots)\n\nGo's `net/http` decodes `%2E` → `.` in `r.URL.Path` before any middleware runs,\nso `%2E%2E` arrives as `..` by the time any of the options below apply. All options\noperate on the already-decoded `r.URL.Path` and therefore handle encoded dots\nwithout any extra work.\n\n### Fix discussion\n\nAll options below address the same root problem: `r.URL.RequestURI()` preserves\ndot segments while `r.URL.Path` has already resolved them, and auth sees the\nun-resolved form. The options differ in where the resolution happens and how\ninvasive the change is.\n\n#### Option A — Reject requests containing dot segments\n\nReject (400 Bad Request) any request whose decoded path contains `/./` or `/../`:\n\n```go\nfunc rejectDotSegmentsMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        p := r.URL.Path\n        if strings.Contains(p, \"/./\") || strings.Contains(p, \"/../\") ||\n            strings.HasSuffix(p, \"/.\") || strings.HasSuffix(p, \"/..\") {\n            http.Error(w, \"Bad Request\", http.StatusBadRequest)\n            return\n        }\n        next.ServeHTTP(w, r)\n    })\n}\n```\n\n**Pros:** Simple, explicit, no normalization side-effects.  \n**Cons:** Rejects requests that some clients may legitimately send (though dot\nsegments in HTTP paths are unusual and ill-advised).\n\n#### Option B — Use `path.Clean`\n\n```go\nfunc cleanPathMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        p := r.URL.Path\n        cleaned := path.Clean(p)\n        if cleaned != p {\n            r2 := r.Clone(r.Context())\n            r2.URL.Path = cleaned\n            r2.URL.RawPath = \"\"\n            next.ServeHTTP(w, r2)\n            return\n        }\n        next.ServeHTTP(w, r)\n    })\n}\n```\n\n`path.Clean` resolves `..` and `.`, collapses double slashes, and also removes\nthe trailing slash. The trailing-slash removal is a breaking change for any config\nthat uses paths ending in `/` — resource patterns, mount paths, or anything else\nmatched against the incoming path. A request to `/upload/foo/drafts/` would be\ncleaned to `/upload/foo/drafts`, and any pattern or handler that expects the\ntrailing slash would no longer match.\n\nThis can be mitigated by restoring the trailing slash after cleaning:\n\n```go\nif len(p) > 1 && p[len(p)-1] == '/' {\n    cleaned += \"/\"\n}\n```\n\n**Implementation note:** An approach that stores the cleaned path in the request\ncontext rather than modifying `r.URL.Path` and clearing `r.URL.RawPath` will not\nwork: both the auth middleware and the bucket handler read from `r.URL` directly,\nso a context-stored override is invisible to them.\n\n**Pros:** Uses the standard library; less custom code.  \n**Cons:** The trailing-slash removal is mitigable by restoring the trailing slash\nafter cleaning (as shown above), but it adds a correctness requirement to the\nmiddleware that is easy to overlook — omitting it silently breaks any config using\ntrailing-slash patterns, which is the default convention in s3-proxy examples and\ndocumentation.\n\n---\n\n## Interaction between Issue 2 and Issue 3 fixes\n\nThe choice made for Issue 2 affects the tradeoffs for Issue 3:\n\n- If **Option A** is chosen for Issue 2 (auth uses `r.URL.Path`), then dot segments\n  have already been resolved by Go before any middleware runs, so Issue 3 is\n  partially addressed without any additional middleware — but Option A's RFC\n  non-compliance tradeoff still applies.\n- If **Option B** is chosen for Issue 2 (raw path in both layers), the auth\n  middleware sees the encoded form, which still contains literal `../` dot segments.\n  Issue 3 is **not** addressed by Option B alone — one of the Issue 3 options must\n  also be applied. Importantly, whichever dot-segment option is chosen must clear\n  `r.URL.RawPath` when it modifies the path, so that `r.URL.EscapedPath()` in the\n  bucket handler reflects the cleaned path. This works naturally with both Issue 3\n  options (which operate on `r.URL.Path` and clear `RawPath`), and the fixes\n  compose cleanly in practice.\n- In all cases, an explicit dot-segment policy (reject or resolve) is clearer than\n  relying on Go's implicit resolution as a side-effect.\n\n---\n\n## Combined effect\n\n| Attack | Issue 1 fix | Issue 2 fix | Issue 3 fix |\n|---|---|---|---|\n| `*` crosses `/` (`/upload/*/drafts/` matches `../restricted/`) | Fixed | — | — |\n| `%2F` segment injection (`foo%2Frestricted/drafts/` bypasses `*/restricted/`) | No | Fixed | — |\n| `..` traversal via `**` prefix pattern (`/open/../restricted/`) | No | No | Fixed |\n| `%2F..%2F` encoded traversal (decoded `..` consumed by `**`) | No | Fixed* | Fixed |\n\n\\* Issue 2's fix (auth using decoded path, Option A) also prevents `%2F`-encoded\ndot segments from being treated as opaque tokens, so the decoded `..` is visible\nto the glob before matching.\n\n---\n\n## Suggested combination of fixes\n\n- **Issue 1:** Pass `'/'` as the separator to `glob.Compile`. Unambiguously correct; `*` should never have crossed `/`.\n- **Issue 2:** Option B — use the raw path (`r.URL.EscapedPath()`) in both the auth middleware and the bucket handler. This is the only option that avoids client-side breaking changes for operators whose clients encode user input containing `/` as `%2F`. The security guarantee is namespace separation, which is the right model: the server has no basis to distinguish a legitimate `%2F`-encoded identifier from one that \"looks like\" a traversal attempt, so consistent handling at both layers is the correct responsibility boundary.\n- **Issue 3:** Option B — `cleanPathMiddleware` using `path.Clean` with trailing slash restored. Required when using Issue 2 Option B, since auth still sees the raw path. The two fixes compose cleanly: the middleware modifies `r.URL.Path` and clears `r.URL.RawPath`, so `r.URL.EscapedPath()` in the bucket handler reflects the cleaned path.\n\nThe combined breaking change is limited to config files: operators need to replace `*` with `**` wherever multi-segment wildcard matching is intended. Client-facing URLs require no changes.\n\n---\n\n\n## Resources\n\n- `pkg/s3-proxy/authx/authentication/main.go` — `findResource`, the `glob.Compile` call\n- `pkg/s3-proxy/server/server_integration_test.go` — `TestPercentEncodedSlashBypass`, `TestPathTraversalDoubleStarPrefix`, `TestPathCleaning`\n- `github.com/gobwas/glob` — separator documentation\n- RFC 3986 §2.2 — equivalence of percent-encoded reserved characters\n- RFC 3986 §3.3 — path segment semantics\n- RFC 3986 §5.2.4 — dot-segment resolution in URI paths","state":0,"publishedAt":"2026-05-05T18:52:56+00:00","modifiedAt":"2026-06-25T23:11:23.6493984+00:00","affected":[{"ecosystem":"Go","name":"github.com/oxyno-zeta/s3-proxy","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"0.0.0-20260424211602-1320e4abd46a"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"CRITICAL"}],"references":["https://github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662p","https://nvd.nist.gov/vuln/detail/CVE-2026-42882","https://github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51","https://github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48a","https://github.com/oxyno-zeta/s3-proxy"],"sourceUrl":"https://github.com/advisories/GHSA-rfgq-wgg8-662p","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-rfgq-wgg8-662p","identifiers":["GHSA-rfgq-wgg8-662p","CVE-2026-42882","GO-2026-5616"]},"retained":false,"historicalAffected":[]},{"revision":"30832c88d1d34afc855cb6fc248554a6","observedAt":"2026-10-24T13:19:41.952+00:00","statement":{"source":"osv","id":"GO-2026-5616","aliases":["CVE-2026-42882","GHSA-rfgq-wgg8-662p"],"title":"S3-Proxy has Security Issues in its Resource Path Matching Implementation in github.com/oxyno-zeta/s3-proxy","description":"S3-Proxy has Security Issues in its Resource Path Matching Implementation in github.com/oxyno-zeta/s3-proxy","state":0,"publishedAt":"2026-06-25T22:34:34+00:00","modifiedAt":"2026-06-25T23:01:33.7681068+00:00","affected":[{"ecosystem":"Go","name":"github.com/oxyno-zeta/s3-proxy","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"0.0.0-20260424211602-1320e4abd46a"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[],"references":["https://github.com/oxyno-zeta/s3-proxy/security/advisories/GHSA-rfgq-wgg8-662p","https://nvd.nist.gov/vuln/detail/CVE-2026-42882","https://github.com/oxyno-zeta/s3-proxy/commit/1320e4abd46ad18c2851fedde50dbb79df8b7a51","https://github.com/oxyno-zeta/s3-proxy/commit/af5ff57d8c6022459495b8fb50130073bca7b48a"],"sourceUrl":"https://osv.dev/vulnerability/GO-2026-5616","license":"OSV · upstream source license applies","key":"osv:GO-2026-5616","identifiers":["GO-2026-5616","CVE-2026-42882","GHSA-rfgq-wgg8-662p"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-07T16:07:52.6079466+00:00","complete":true,"records":1769,"lastAttemptAt":"2026-11-07T16:07:52.6079466+00:00","lastSuccessAt":"2026-11-07T16:07:52.8396759+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.8396778+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-bb5c9d5a08063ef076042e33dd72a7d0ffa4c4487efdcf3106e29ba55ec2ad57-c173d89a3f1a0bdb31cd68e866be332a5016302f586ede89a35ec193ac99504a","complete":true,"records":420933,"lastAttemptAt":"2026-11-07T16:07:52.127477+00:00","lastSuccessAt":"2026-11-07T16:07:52.0557652+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.127477+00:00","snapshotPublishedAt":"2026-11-07T15:22:48+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-bee3dfbd77fe4043986590b0e0b76259","complete":true,"records":13633,"lastAttemptAt":"2026-11-07T16:18:44.6437051+00:00","lastSuccessAt":"2026-11-07T16:18:44.639+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:18:44.639+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-07T16:07:52.6079466+00:00","complete":true,"records":1096,"lastAttemptAt":"2026-11-07T16:07:52.6079466+00:00","lastSuccessAt":"2026-11-07T16:07:58.5710341+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:58.5710358+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-e1c431e1022f4db1b4e459e61c31a4ca","complete":true,"records":13645,"lastAttemptAt":"2026-11-07T16:08:00.6485737+00:00","lastSuccessAt":"2026-11-07T16:08:00.644+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:08:00.644+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-0eb4d6dde62dffacd040ad992d4fd5a65cd7d0dc5f5f4cfa8b41967fc6f9ca5f","complete":true,"records":9914,"lastAttemptAt":"2026-11-07T16:07:52.5107257+00:00","lastSuccessAt":"2026-11-06T20:10:36.6450506+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.5107257+00:00","snapshotPublishedAt":"2026-11-06T20:02:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-fde804134ecdda0ddd5ff5c26ee1fff5a6fa659311f7ef2e51c6ad54964db0e2","complete":true,"records":1971,"lastAttemptAt":"2026-11-07T16:07:52.2947928+00:00","lastSuccessAt":"2026-11-06T19:03:17.0399903+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.2947928+00:00","snapshotPublishedAt":"2026-11-06T18:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-335633e78a15c9cde10a6025895b5617717087fb945489ae7e15d377bacf34a9","complete":true,"records":3055,"lastAttemptAt":"2026-11-07T16:07:52.5258708+00:00","lastSuccessAt":"2026-11-06T16:03:00.7615789+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.5258708+00:00","snapshotPublishedAt":"2026-11-06T16:02:48+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-40ad6183a951565ace1520a9b4d631e1d0bfb1d4bf7c5574c375032559f3ff53","complete":true,"records":231292,"lastAttemptAt":"2026-11-07T16:07:52.4004477+00:00","lastSuccessAt":"2026-11-07T10:29:00.4821259+00:00","error":null,"lastVerifiedAt":"2026-11-07T16:07:52.4004477+00:00","snapshotPublishedAt":"2026-11-07T09:32:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}