{"id":"GHSA-69hv-5jfc-mvmv","kind":"vulnerability","aliases":["GHSA-69hv-5jfc-mvmv"],"related":[],"title":"rewrite re-decodes {http.matchers.file.remainder}, bypassing path-based deny rules","description":"### Summary\r\n\r\nThe 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.\r\n\r\n### Details\r\n\r\n`{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.\r\n\r\n`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).\r\n\r\nFix: 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`.\r\n\r\nDiscovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-32RAL55Q.\r\n\r\n### Proof of concept\r\n\r\n`public/index.php` and `public/private/secret.txt` exist:\r\n\r\n```caddyfile\r\n:8080 {\r\n\troot * ./public\r\n\troute {\r\n\t\t@denied path /private/*\r\n\t\terror @denied 403\r\n\t\t@php file {\r\n\t\t\ttry_files {path} index.php\r\n\t\t\tsplit_path .php\r\n\t\t}\r\n\t\trewrite @php {http.matchers.file.relative}{http.matchers.file.remainder}\r\n\t\t@phpfiles path *.php /index.php/*\r\n\t\trespond @phpfiles \"PHP route: {uri}\" 200\r\n\t\tfile_server\r\n\t}\r\n}\r\n```\r\n\r\n```console\r\n$ curl 'http://localhost:8080/index.php/..%252fprivate%252fsecret.txt'\r\nSECRET\r\n$ curl 'http://localhost:8080/index.php/x%23y?a=b'\r\nPHP route: /index.php/x?a=b\r\n$ curl 'http://localhost:8080/index.php/x%253Fa=evil?a=b'\r\nPHP route: /index.php/x%3Fa=evil?a=b\r\n```\r\n\r\nThe 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.\r\n\r\n### Impact\r\n\r\nUnauthenticated 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.","summary":"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.","severity":"medium","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"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"],"how_to_check":"- Run `caddy version` and compare against the fixed release.\n- 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).\n- 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`.","status":"open","published_at":"2026-11-04T00:59:57+00:00","first_seen_at":"2026-11-09T22:53:33+00:00","triaged_at":"2026-11-09T22:58:44+00:00","updated_at":"2026-11-09T22:58:44+00:00","category":"runtime","confidence":0.85,"triage":{"at":"2026-11-09T22:58:44Z","model":"claude-haiku-5-5","output":{"fix":"- Caddy (Go module `github.com/caddyserver/caddy/v2`): upgrade to `2.11.8` or later; affected versions are `<= 2.11.7`.","reach":"internet","title":"Caddy: rewrite re-decodes path remainder, bypassing path-based deny rules","impact":"medium","summary":"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.","audience":"businesses","category":"runtime","nicknames":[],"who_cares":["caddy","go","php-server","frankenphp","self-hosted"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"Unauthenticated clients read files that a deny rule should protect","analyst_note":"","how_to_check":"- Run `caddy version` and compare against the fixed release.\n- 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).\n- Check whether any `path` or `file` deny rule sits before such a rewrite.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-remainder-rewrite","no-path-deny-rule","static-export","managed-hosting"],"who_is_affected":"Sites running Caddy 2.11.7 or earlier that rewrite requests to `{http.matchers.file.remainder}`, typically PHP-style path info set up by `try_files` with `split_path`, and that protect paths with a deny rule placed before that rewrite. Configurations without such a rewrite, or without a path-based deny rule, are not exposed to the file-read bypass.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000902,"input_tokens":5537,"output_tokens":697,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-174':27B '-2026':265C '-32':266C '/caddyserver/caddy/v2':24A '/foo/bar':241C '/index.php':302C '/index.php/../private/secret.txt':358C '/index.php/x':326C,338C '/private':282C '/public':278C '200':308C '23':136C '23y':321C '252f':63B,122C '252fprivate':315C '252fsecret.txt':316C '253fa':332C '3f':134C '3fa':339C '403':285C '5jfc':15A,20A '69hv':14A,19A '8080':276C '8080/index.php':314C '8080/index.php/x':320C,331C 'alreadi':112C 'also':403C 'alter':85B 'ant':264C 'anthrop':258C 'b':323C,328C,335C,342C 'backend':92B,411C 'base':9A,76B,130C,380C 'becom':64B,123C,357C 'block':82B 'bypass':6A 'caddi':25B,37B 'caddyfil':275C 'chang':404C 'charact':238C 'client':67B,372C 'concept':270C 'consol':311C 'curl':312C,318C,329C 'cut':177C 'cwe':26B 'decod':4A,52B,104C,152C,185C 'deni':10A,77B,131C,280C,284C,352C,389C 'detail':149C 'discov':256C 'doubl':58B,116C 'double-encod':57B,115C 'dungla':250C 'encod':59B,117C 'error':283C 'escap':47B,157C,227C,253C 'escapepathplacehold':155C 'escapepathpreservingslash':231C 'evil':333C,340C 'exist':274C 'file':43B,71B,126C,200C,254C,287C,289C,309C,364C,369C,375C 'first':344C 'fix':226C 'frankenphp':28B,220C 'generat':224C 'ghsa':13A,18A 'ghsa-69hv-5jfc-mvmv':12A,17A 'github.com':23A 'github.com/caddyserver/caddy/v2':22A 'go':29B 'handler':40B,97C,173C 'hold':151C 'host':36B 'http.matchers.file.relative':160C,208C,233C,297C 'http.matchers.file.remainder':5A,99C,150C,209C,298C,395C 'http.request.uri.path':158C 'impact':370C 'index.php':291C 'info':119C,140C,219C,400C 'insert':41B,98C,174C 'like':232C 'localhost':313C,319C,330C 'matcher':44B,111C,192C 'meant':80B,387C 'miss':359C 'modules/caddyhttp/rewrite/rewrite.go':156C 'mvmv':16A,21A 'opaqu':349C 'origin':197C 'oss':260C 'pars':166C 'patch':246C 'path':8A,60B,75B,90B,118C,129C,139C,148C,153C,198C,203C,218C,281C,290C,293C,300C,379C,399C,409C 'path-bas':7A,74B,128C,378C 'percent':51B,103C,184C 'percent-decod':50B,102C,183C 'php':31B,216C,222C,286C,294C,296C,301C,305C,324C,336C,361C,397C 'php-server':30B 'php-style':215C,396C 'phpfile':299C,304C 'place':382C 'pr':248C 'proof':268C 'protect':133C,368C 'public':247C 'public/index.php':271C 'public/private/secret.txt':273C 'queri':87B,143C,406C 'ral55q':267C 'ran':113C,194C 'raw':176C 're':3A 're-decod':2A 'reach':125C 'read':69B,373C 'receiv':93B,412C 'remaind':45B,170C,229C,235C,255C 'replac':141C 'request':345C 'reserv':237C 'respond':303C 'result':179C 'rewrit':1A,39B,96C,172C,206C,252C,295C,355C,385C,393C 'rewrite-escape-file-remaind':251C 'rewritten':106C,163C,243C 'root':277C 'rout':279C,306C,325C,337C,362C 'rule':11A,78B,132C,381C 'runtim':33B 'scanner':261C 'second':188C 'secret':317C 'segment':350C 'self':35B 'self-host':34B 'serv':366C 'server':32B,223C,310C,365C 'setup':213C 'singl':348C 'site':391C 'split':202C,292C 'static':70B,374C 'string':88B,144C,407C 'style':217C,398C 'summari':94C 'text':154C 'time':189C 'tri':199C,288C 'truncat':146C 'unauthent':66B,371C 'unescap':100C 'uri':107C,164C,307C 'usual':212C 'vulner':263C 'without':46B,236C","nicknames":[],"mitigations":[],"who_affected":"Sites running Caddy 2.11.7 or earlier that rewrite requests to `{http.matchers.file.remainder}`, typically PHP-style path info set up by `try_files` with `split_path`, and that protect paths with a deny rule placed before that rewrite. Configurations without such a rewrite, or without a path-based deny rule, are not exposed to the file-read bypass.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["githubcomcaddyservercaddyv2"],"ptokens":["caddy","caddyserver","com","github","v2"],"labels":["caddy","cwe174","frankenphp","go","medium","phpserver","runtime","selfhosted"],"campaign_type":null,"search_a":"'/caddyserver/caddy/v2':24A '5jfc':15A,20A '69hv':14A,19A 'base':9A 'bypass':6A 'decod':4A 'deni':10A 'ghsa':13A,18A 'ghsa-69hv-5jfc-mvmv':12A,17A 'github.com':23A 'github.com/caddyserver/caddy/v2':22A 'http.matchers.file.remainder':5A 'mvmv':16A,21A 'path':8A 'path-bas':7A 're':3A 're-decod':2A 'rewrit':1A 'rule':11A","search_b":"'-174':27B '252f':63B 'alter':85B 'backend':92B 'base':76B 'becom':64B 'block':82B 'caddi':25B,37B 'client':67B 'cwe':26B 'decod':52B 'deni':77B 'doubl':58B 'double-encod':57B 'encod':59B 'escap':47B 'file':43B,71B 'frankenphp':28B 'go':29B 'handler':40B 'host':36B 'insert':41B 'matcher':44B 'meant':80B 'path':60B,75B,90B 'path-bas':74B 'percent':51B 'percent-decod':50B 'php':31B 'php-server':30B 'queri':87B 'read':69B 'receiv':93B 'remaind':45B 'rewrit':39B 'rule':78B 'runtim':33B 'self':35B 'self-host':34B 'server':32B 'static':70B 'string':88B 'unauthent':66B 'without':46B","search_c":"'-2026':265C '-32':266C '/foo/bar':241C '/index.php':302C '/index.php/../private/secret.txt':358C '/index.php/x':326C,338C '/private':282C '/public':278C '200':308C '23':136C '23y':321C '252f':122C '252fprivate':315C '252fsecret.txt':316C '253fa':332C '3f':134C '3fa':339C '403':285C '8080':276C '8080/index.php':314C '8080/index.php/x':320C,331C 'alreadi':112C 'also':403C 'ant':264C 'anthrop':258C 'b':323C,328C,335C,342C 'backend':411C 'base':130C,380C 'becom':123C,357C 'caddyfil':275C 'chang':404C 'charact':238C 'client':372C 'concept':270C 'consol':311C 'curl':312C,318C,329C 'cut':177C 'decod':104C,152C,185C 'deni':131C,280C,284C,352C,389C 'detail':149C 'discov':256C 'doubl':116C 'double-encod':115C 'dungla':250C 'encod':117C 'error':283C 'escap':157C,227C,253C 'escapepathplacehold':155C 'escapepathpreservingslash':231C 'evil':333C,340C 'exist':274C 'file':126C,200C,254C,287C,289C,309C,364C,369C,375C 'first':344C 'fix':226C 'frankenphp':220C 'generat':224C 'handler':97C,173C 'hold':151C 'http.matchers.file.relative':160C,208C,233C,297C 'http.matchers.file.remainder':99C,150C,209C,298C,395C 'http.request.uri.path':158C 'impact':370C 'index.php':291C 'info':119C,140C,219C,400C 'insert':98C,174C 'like':232C 'localhost':313C,319C,330C 'matcher':111C,192C 'meant':387C 'miss':359C 'modules/caddyhttp/rewrite/rewrite.go':156C 'opaqu':349C 'origin':197C 'oss':260C 'pars':166C 'patch':246C 'path':118C,129C,139C,148C,153C,198C,203C,218C,281C,290C,293C,300C,379C,399C,409C 'path-bas':128C,378C 'percent':103C,184C 'percent-decod':102C,183C 'php':216C,222C,286C,294C,296C,301C,305C,324C,336C,361C,397C 'php-style':215C,396C 'phpfile':299C,304C 'place':382C 'pr':248C 'proof':268C 'protect':133C,368C 'public':247C 'public/index.php':271C 'public/private/secret.txt':273C 'queri':143C,406C 'ral55q':267C 'ran':113C,194C 'raw':176C 'reach':125C 'read':373C 'receiv':412C 'remaind':170C,229C,235C,255C 'replac':141C 'request':345C 'reserv':237C 'respond':303C 'result':179C 'rewrit':96C,172C,206C,252C,295C,355C,385C,393C 'rewrite-escape-file-remaind':251C 'rewritten':106C,163C,243C 'root':277C 'rout':279C,306C,325C,337C,362C 'rule':132C,381C 'scanner':261C 'second':188C 'secret':317C 'segment':350C 'serv':366C 'server':223C,310C,365C 'setup':213C 'singl':348C 'site':391C 'split':202C,292C 'static':374C 'string':144C,407C 'style':217C,398C 'summari':94C 'text':154C 'time':189C 'tri':199C,288C 'truncat':146C 'unauthent':371C 'unescap':100C 'uri':107C,164C,307C 'usual':212C 'vulner':263C 'without':236C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-04T00:59:57+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-69hv-5jfc-mvmv","url":"https://github.com/caddyserver/caddy/security/advisories/GHSA-69hv-5jfc-mvmv","severity":"medium","published_at":"2026-11-04T00:59:57+00:00","modified_at":"2026-11-04T01:01:50+00:00","first_seen_at":"2026-11-09T22:53:33+00:00","fetched_at":"2026-11-09T22:53:33+00:00","note":null}],"documents":[{"id":110736,"url":"https://github.com/caddyserver/caddy/security/advisories/GHSA-69hv-5jfc-mvmv","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}