GitHub Security Advisory: GHSA-rfgq-wgg8-662p
Package: github.com/oxyno-zeta/s3-proxy (GO)
Severity: CRITICAL (CVSS 9.4)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-863: Incorrect Authorization
Vulnerable versions: < 0.0.0[PII_REDACTED]1602-1320e4abd46a
Patched in: 0.0.0[PII_REDACTED]1602-1320e4abd46a

Summary: S3-Proxy has Security Issues in its Resource Path Matching Implementation

Details:
## Background

The 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.

Rather 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.

## Root cause: two different path representations

Go's `net/http` decodes percent-encoded characters when it parses an incoming URL:
`%2F` becomes `/` in `r.URL.Path`, while the original encoded form is preserved in
`r.URL.RawPath`. Two different parts of s3-proxy use different fields:

- The **auth middleware** calls `r.URL.RequestURI()`, which returns the encoded
  form (from `RawPath` when available). It sees `%2F` as literal characters, not
  as path separators.
- The **bucket handler** reads `r.URL.Path` to build the S3 key. It sees the
  decoded form, where `%2F` has already become `/`.

All three issues stem from this mismatch, combined with how glob patterns are
compiled. The examples below use PUT for concreteness, but the auth bypass applies
to any HTTP method — a config that restricts GET or DELETE on a namespace is
equally affected, meaning an attacker could read from or delete objects in a
protected namespace without credentials.

### A note on RFC 3986

RFC 3986 §2.2 states that `/` and `%2F` are **not equivalent** in a URI path:

> URIs that differ in the replacement of a reserved character with its
> corresponding percent-encoded octet are **not** equivalent.

`/` is a reserved gen-delim used as a path segment separator. `%2F` is its
percent-encoded form and, by the RFC, should be treated as data *within* a
segment — not as a separator. So:

- `/foo/bar/baz` → three segments: `foo`, `bar`, `baz`
- `/foo%2Fbar/baz` → two segments: `foo/bar` (opaque data), `baz`

The original functional concern (wanting `foo%2Fbar` to match as a single token
against a single-segment wildcard) is therefore RFC-correct behaviour. Go's
`r.URL.Path` violates this by decoding `%2F` to `/`, collapsing the two
representations into one. This is the underlying tension that makes fixing these
issues non-trivial: the simplest security fix makes s3-proxy *more* RFC
non-compliant, while the RFC-correct fix requires a more significant refactor.

### A note on breaking changes

Any of the proposed fixes for these issues should be treated as a **breaking
change**. Each option alters how path patterns in existing configs are interpreted
— whether by changing how `*` matches segments, by shifting which path
representation auth matches against, or by normalising paths before they reach the
router. Operators upgrading to