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

s3-proxy auth matched globs against the encoded URI while the bucket handler used the decoded path, letting unauthenticated PUTs write into protected S3 namespaces.

- Payout: Research
- Severity: critical
- Program: s3-proxy
- Found by: argos83
- Disclosed: 2026-05-05
- CVE: CVE-2026-42882
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-rfgq-wgg8-662p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rfgq-wgg8-662p
- Tags: area:cloud domain:cloud domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:go tech:s3 technique:parser-differential vuln:access-control vuln:auth-bypass vuln:path-traversal

**Target:** HTTP resource-path auth middleware and S3 bucket handler
**Root cause:** glob.Compile is called without a '/' separator so '*' crosses '/', and auth matches r.URL.RequestURI() (encoded, dot segments preserved) while the bucket handler builds the S3 key from r.URL.Path (decoded, dot segments resolved).
**Steps:**
1. Configure an open route `path: /upload/*/drafts/` (whiteList) and a protected route `path: /upload/*/restricted/` with basic auth.
2. Send `PUT /upload/foo/drafts/../restricted/` — '*' crosses '/' so the open pattern matches and the request is accepted unauthenticated.
3. Alternatively send `PUT /upload/foo%2Frestricted/drafts/`: auth sees one encoded segment `foo%2Frestricted` matching `*`, but r.URL.Path decodes to `/upload/foo/restricted/drafts/`, writing into the restricted namespace.
4. With `**` prefix patterns (`/open/**` open, `/restricted/**` protected), send `PUT /open/../restricted/secret.json`: auth matches `/open/**` on the raw path, while r.URL.Path is already `/restricted/secret.json`, landing the object in the protected prefix.
5. Confirm via integration tests TestPercentEncodedSlashBypass and TestPathTraversalDoubleStarPrefix, which return 204 instead of 401.
**How found:** Started from a functional complaint that `some%2Fvalue` should match a single-segment wildcard; investigating why %2F was decoded and how routes matched surfaced the encoded-vs-decoded mismatch and the separator-less glob compile.
**Exploitation:** Unauthenticated attacker sends crafted PUT (and equivalently GET/DELETE) requests whose raw path matches a whitelisted glob but whose decoded path resolves into a protected namespace, writing, reading or deleting objects there without credentials.
**Escalation:** Applies to any HTTP method, so a config restricting GET or DELETE on a namespace allows unauthenticated read or deletion of protected objects, not just writes.
**Insight:** When auth and the resource handler read different URL representations (RequestURI vs Path), any encoding that changes segment count — %2F, dot segments, %2E — becomes an auth bypass; check every layer that derives a key or route from the URL uses the same field.

## Proof of concept (copied exactly from the original report)

**Percent-encoded slash auth bypass request**

```
PUT /upload/foo%2Frestricted/drafts/

Step by step:

1. `r.URL.RawPath` = `/upload/foo%2Frestricted/drafts/`
2. `r.URL.Path` (decoded) = `/upload/foo/restricted/drafts/`
3. Auth middleware calls `r.URL.RequestURI()` → returns the encoded form.
4. With Fix 1's separator `/`, glob splits on the literal `/`. The segment between
   the first and second slash is `foo%2Frestricted` — one token with no literal `/`
   — so `*` matches it. Pattern `/upload/*/drafts/` fires.
5. Open route → request proceeds without auth.
6. Bucket handler uses `r.URL.Path` → S3 key is `upload/foo/restricted/drafts/…`
   — **written into the restricted namespace without credentials**.
```

**Dot-dot traversal bypass request**

```
PUT /open/../restricted/secret.json

Step by step:

1. Go's `net/url` resolves dot segments when parsing the request URI: `r.URL.Path`
   is `/restricted/secret.json`. The raw form `../` is preserved only in
   `r.URL.RawPath`.
2. The auth middleware calls `r.URL.RequestURI()`, which returns the encoded
   form — `/open/../restricted/secret.json` — and evaluates resources against that.
3. `/restricted/**` does not match because the raw path does not start with `/restricted/`.
4. `/open/**` matches: `**` is allowed to cross `/`, so it consumes `../restricted/secret.json`.
5. The open route fires — no auth required — the request returns 204.
6. The bucket handler reads `r.URL.Path` — already `/restricted/secret.json` — and
   writes the file directly into the restricted namespace.
```

**Integration test for traversal via ** prefix**

```
name:         "traversal from open to restricted via ** prefix pattern is blocked",
    inputMethod:  "PUT",
    inputURL:     "http://localhost/open/../restricted/file.txt",
    expectedCode: 401,
```

