---
id: "GHSA-69hv-5jfc-mvmv"
page: "https://infrastack.ai/live/GHSA-69hv-5jfc-mvmv"
kind: "vulnerability"
aliases: ["GHSA-69hv-5jfc-mvmv"]
title: "rewrite re-decodes {http.matchers.file.remainder}, bypassing path-based deny rules"
severity: "medium"
cvss: null
epss: null
exploited: false
status: "open"
category: "runtime"
products: [{"name": "github.com/caddyserver/caddy/v2", "fixed": ["2.11.8"], "affected": "<= 2.11.7", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["caddy", "cwe-174", "frankenphp", "go", "php-server", "runtime", "self-hosted"]
not_affected_if: ["managed-hosting", "no-path-deny-rule", "no-remainder-rewrite", "static-export"]
published_at: "2026-11-04T00:59:57+00:00"
first_seen_at: "2026-11-09T22:53:33+00:00"
updated_at: "2026-11-09T22:58:44+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/caddyserver/caddy/security/advisories/GHSA-69hv-5jfc-mvmv", "first_seen_at": "2026-11-09T22:53:33+00:00"}]
exploit_sources: []
---

# rewrite re-decodes {http.matchers.file.remainder}, bypassing path-based deny rules

Link for people: https://infrastack.ai/live/GHSA-69hv-5jfc-mvmv (this .md is for agents)

Caddy's rewrite handler inserts the file matcher remainder without escaping and then percent-decodes it again, so a double-encoded path such as `..%252f` becomes `../`. An unauthenticated client can read static files that a path-based deny rule was meant to block, and can alter the query string or path the backend receives.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/caddyserver/caddy/v2 affected <= 2.11.7 fixed 2.11.8

## How to check

- Run `caddy version` and compare against the fixed release.
- Search the Caddyfile for `rewrite` directives that use `{http.matchers.file.remainder}`, usually paired with `try_files` and `split_path` (the pattern FrankenPHP's `php_server` generates).
- Check whether any `path` or `file` deny rule sits before such a rewrite.

## Fix

- Caddy (Go module `github.com/caddyserver/caddy/v2`): upgrade to `2.11.8` or later; affected versions are `<= 2.11.7`.

## Timeline

- 2026-11-09T22:53:33+00:00 ghsa-repo https://github.com/caddyserver/caddy/security/advisories/GHSA-69hv-5jfc-mvmv

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/caddyserver/caddy/security/advisories/GHSA-69hv-5jfc-mvmv

## From the sources

### Summary

The rewrite handler inserts `{http.matchers.file.remainder}` unescaped, then percent-decodes the rewritten URI again after the matchers already ran. A double-encoded path info such as `..%252f` becomes `../` and reaches files a path-based deny rule protects; `%3F` and `%23` in the path info replace the query string or truncate the path.

### Details

`{http.matchers.file.remainder}` holds decoded path text. `escapePathPlaceholders()` (`modules/caddyhttp/rewrite/rewrite.go`) escapes `{http.request.uri.path}` and `{http.matchers.file.relative}` before the rewritten URI is parsed, but not the remainder. The rewrite handler inserts it raw, cuts the result at `#` and `?`, and percent-decodes it a second time, after the matchers that ran on the original path.

`try_files` with `split_path` and a rewrite to `{http.matchers.file.relative}{http.matchers.file.remainder}` is the usual setup for PHP-style path info (FrankenPHP's `php_server` generated it).

Fix: escape the remainder with `escapePathPreservingSlashes()`, like `{http.matchers.file.relative}`. A remainder without reserved characters, such as `/foo/bar`, is rewritten as before. Patch: public PR from `dunglas:rewrite-escape-file-remainder`.

Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-32RAL55Q.

### Proof of concept

`public/index.php` and `public/private/secret.txt` exist:

```caddyfile
:8080 {
	root * ./public
	route {
		@denied path /private/*
		error @denied 403
		@php file {
			try_files {path} index.php
			split_path .php
		}
		rewrite @php {http.matchers.file.relative}{http.matchers.file.remainder}
		@phpfiles path *.php /index.php/*
		respond @phpfiles "PHP route: {uri}" 200
		file_server
	}
}
```

```console
$ curl 'http://localhost:8080/index.php/..%252fprivate%252fsecret.txt'
SECRET
$ curl 'http://localhost:8080/index.php/x%23y?a=b'
PHP route: /index.php/x?a=b
$ curl 'http://localhost:8080/index.php/x%253Fa=evil?a=b'
PHP route: /index.php/x%3Fa=evil?a=b
```

The first request is a single opaque segment for `@denied`. After the rewrite it becomes `/index.php/../private/secret.txt`, misses the PHP route and `file_server` serves the protected file.

### Impact

Unauthenticated clients read static files that a path-based rule placed before the rewrite is meant to deny, on sites that rewrite to `{http.matchers.file.remainder}` (PHP-style path info). They can also change the query string or path the backend receives.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
