{"id":"GHSA-25cw-38mq-q7m6","kind":"vulnerability","aliases":["GHSA-25cw-38mq-q7m6"],"related":[],"title":"file matcher resolves /file/./ to a regular file, bypassing suffix path rules","description":"### Summary\r\n\r\n`try_files {path}` resolves `/.env/./` to the regular file `/.env`, while path matchers see `/.env/`. A rule such as `path *.env` placed before the rewrite doesn't match, and `file_server` serves the file without its canonical redirect. `php_fastcgi` and FrankenPHP's `php_server` are affected through their built-in try_files route.\r\n\r\n### Details\r\n\r\nThe `file` matcher (`modules/caddyhttp/fileserver/matcher.go`) runs `path.Clean` on each expanded candidate, which drops the trailing slash, and restores it only when the try_files pattern itself ends in `/`. So `try_files {path}` resolves `/.env/./` to the regular file `/.env` and rewrites the request to it.\r\n\r\nPath matchers see `/.env/` for that request, so suffix rules, exact paths, globs ending in a file name and `$`-anchored `path_regexp` evaluated before the rewrite don't match. `file_server` then skips its canonical redirect because `path.Base` of the original path (`.`) and of the rewritten path (`.env`) differ, and serves the file.\r\n\r\nFix: keep the trailing slash of the expanded candidate through `path.Clean`. `strictFileExists` already requires a candidate ending in `/` to be a directory, so `/.env/` and `/.env/./` no longer match the regular file, while `/dir/./` still matches the directory and `split_path` keeps working. Patch: public PR from `dunglas:fileserver-match-trailing-slash`.\r\n\r\nDiscovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-VAGWNYYZ.\r\n\r\n### Proof of concept\r\n\r\n```caddyfile\r\n:8080 {\r\n\troot /srv\r\n\t@blocked path *.env\r\n\troute {\r\n\t\trespond @blocked 403\r\n\t\ttry_files {path} {path}/ /index.html\r\n\t\tfile_server\r\n\t}\r\n}\r\n```\r\n\r\n```console\r\n$ curl -i localhost:8080/.env                        # 403\r\n$ curl -i --path-as-is localhost:8080/.env/./        # 200, body of /srv/.env\r\n```\r\n\r\nThe same works with `php_fastcgi` followed by `file_server`.\r\n\r\n### Impact\r\n\r\nUnauthenticated clients read static files (`.env`, backups, configuration) that a suffix or exact path rule is meant to block. Prefix rules and `*/.*` still block. Nothing gets executed.","summary":"Caddy's file matcher turns a request like /.env/./ into the regular file /.env, so path rules written to block that file are skipped. An unauthenticated client can read blocked static files such as .env, backups or configuration.","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-41","frankenphp","package","php","self-hosted"],"not_affected_if":["managed-hosting","no-try-files-path","prefix-rules-only"],"how_to_check":"- Run `caddy version` and compare against the fixed release.\n- Look in the Caddyfile for `try_files {path}` combined with `path` or `path_regexp` block rules, or for `php_fastcgi` or FrankenPHP `php_server` followed by `file_server`.","fix":"- github.com/caddyserver/caddy/v2: upgrade to `2.11.8`","status":"open","published_at":"2026-11-04T00:59:36+00:00","first_seen_at":"2026-11-09T22:53:33+00:00","triaged_at":"2026-11-09T22:58:46+00:00","updated_at":"2026-11-09T22:58:46+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-09T22:58:46Z","model":"claude-haiku-5-5","output":{"fix":"- github.com/caddyserver/caddy/v2: upgrade to `2.11.8`","reach":"internet","title":"Caddy: try_files path trick serves files that path rules block","impact":"medium","summary":"Caddy's file matcher turns a request like /.env/./ into the regular file /.env, so path rules written to block that file are skipped. An unauthenticated client can read blocked static files such as .env, backups or configuration.","audience":"businesses","category":"package","nicknames":[],"who_cares":["caddy","self-hosted","php","frankenphp"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"unauthenticated clients read blocked static files like .env","analyst_note":"","how_to_check":"- Run `caddy version` and compare against the fixed release.\n- Look in the Caddyfile for `try_files {path}` combined with `path` or `path_regexp` block rules, or for `php_fastcgi` or FrankenPHP `php_server` followed by `file_server`.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-try-files-path","prefix-rules-only","managed-hosting"],"who_is_affected":"Sites served by Caddy where a try_files {path} rewrite runs before file_server, or where php_fastcgi or php_server is used, and where path rules are meant to block certain files. Servers that do not use try_files {path} or rely only on prefix rules or directory-wide regex blocks are less exposed. Managed hosting that does not expose the Caddyfile is not affected by the config pattern.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.000828,"input_tokens":5314,"output_tokens":594,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-2026':289C '-41':28B '/.env':43B,48B,78C,83C,88C,162C,167C,177C,250C,252C '/caddyserver/caddy/v2':25A '/dir':260C '/file':4A '/index.html':309C '/srv':297C '/srv/.env':329C '200':326C '25cw':15A,20A '38mq':16A,21A '403':304C,317C '8080':295C '8080/.env':316C,325C 'affect':120C 'alreadi':239C 'anchor':193C 'ant':288C 'anthrop':282C 'backup':70B,347C 'block':54B,64B,298C,303C,359C,364C 'bodi':327C 'built':124C 'built-in':123C 'bypass':9A 'caddi':26B,35B 'caddyfil':294C 'candid':139C,235C,242C 'canon':110C,208C 'client':61B,342C 'concept':293C 'configur':72B,348C 'consol':312C 'curl':313C,318C 'cwe':27B 'detail':129C 'differ':222C 'directori':248C,264C 'discov':280C 'doesn':99C 'drop':141C 'dungla':274C 'end':155C,187C,243C 'env':69B,94C,221C,300C,346C 'evalu':196C 'exact':184C,353C 'execut':367C 'expand':138C,234C 'fastcgi':113C,335C 'file':1A,8A,37B,47B,56B,66B,75C,82C,103C,107C,127C,131C,152C,159C,166C,190C,203C,226C,258C,306C,310C,338C,345C 'fileserv':276C 'fileserver-match-trailing-slash':275C 'fix':227C 'follow':336C 'frankenphp':29B,115C 'get':366C 'ghsa':14A,19A 'ghsa-25cw-38mq-q7m6':13A,18A 'github.com':24A 'github.com/caddyserver/caddy/v2':23A 'glob':186C 'host':34B 'impact':340C 'keep':228C,268C 'like':42B 'localhost':315C,324C 'longer':254C 'match':101C,202C,255C,262C,277C 'matcher':2A,38B,86C,132C,175C 'meant':357C 'modules/caddyhttp/fileserver/matcher.go':133C 'name':191C 'noth':365C 'origin':214C 'oss':284C 'packag':30B 'patch':270C 'path':11A,50B,76C,85C,93C,160C,174C,185C,194C,215C,220C,267C,299C,307C,308C,321C,354C 'path-as-i':320C 'path.base':211C 'path.clean':135C,237C 'pattern':153C 'php':31B,112C,117C,334C 'place':95C 'pr':272C 'prefix':360C 'proof':291C 'public':271C 'q7m6':17A,22A 'read':63B,343C 'redirect':111C,209C 'regexp':195C 'regular':7A,46B,81C,165C,257C 'request':41B,171C,180C 'requir':240C 'resolv':3A,77C,161C 'respond':302C 'restor':146C 'rewrit':98C,169C,199C 'rewritten':219C 'root':296C 'rout':128C,301C 'rule':12A,51B,90C,183C,355C,361C 'run':134C 'scanner':285C 'see':87C,176C 'self':33B 'self-host':32B 'serv':105C,224C 'server':104C,118C,204C,311C,339C 'skip':58B,206C 'slash':144C,231C,279C 'split':266C 'static':65B,344C 'still':261C,363C 'strictfileexist':238C 'suffix':10A,182C,351C 'summari':73C 'trail':143C,230C,278C 'tri':74C,126C,151C,158C,305C 'turn':39B 'unauthent':60B,341C 'vagwnyyz':290C 'vulner':287C 'without':108C 'work':269C,332C 'written':52B","nicknames":[],"mitigations":[],"who_affected":"Sites served by Caddy where a try_files {path} rewrite runs before file_server, or where php_fastcgi or php_server is used, and where path rules are meant to block certain files. Servers that do not use try_files {path} or rely only on prefix rules or directory-wide regex blocks are less exposed. Managed hosting that does not expose the Caddyfile is not affected by the config pattern.","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","cwe41","frankenphp","go","medium","package","php","selfhosted"],"campaign_type":null,"search_a":"'/caddyserver/caddy/v2':25A '/file':4A '25cw':15A,20A '38mq':16A,21A 'bypass':9A 'file':1A,8A 'ghsa':14A,19A 'ghsa-25cw-38mq-q7m6':13A,18A 'github.com':24A 'github.com/caddyserver/caddy/v2':23A 'matcher':2A 'path':11A 'q7m6':17A,22A 'regular':7A 'resolv':3A 'rule':12A 'suffix':10A","search_b":"'-41':28B '/.env':43B,48B 'backup':70B 'block':54B,64B 'caddi':26B,35B 'client':61B 'configur':72B 'cwe':27B 'env':69B 'file':37B,47B,56B,66B 'frankenphp':29B 'host':34B 'like':42B 'matcher':38B 'packag':30B 'path':50B 'php':31B 'read':63B 'regular':46B 'request':41B 'rule':51B 'self':33B 'self-host':32B 'skip':58B 'static':65B 'turn':39B 'unauthent':60B 'written':52B","search_c":"'-2026':289C '/.env':78C,83C,88C,162C,167C,177C,250C,252C '/dir':260C '/index.html':309C '/srv':297C '/srv/.env':329C '200':326C '403':304C,317C '8080':295C '8080/.env':316C,325C 'affect':120C 'alreadi':239C 'anchor':193C 'ant':288C 'anthrop':282C 'backup':347C 'block':298C,303C,359C,364C 'bodi':327C 'built':124C 'built-in':123C 'caddyfil':294C 'candid':139C,235C,242C 'canon':110C,208C 'client':342C 'concept':293C 'configur':348C 'consol':312C 'curl':313C,318C 'detail':129C 'differ':222C 'directori':248C,264C 'discov':280C 'doesn':99C 'drop':141C 'dungla':274C 'end':155C,187C,243C 'env':94C,221C,300C,346C 'evalu':196C 'exact':184C,353C 'execut':367C 'expand':138C,234C 'fastcgi':113C,335C 'file':75C,82C,103C,107C,127C,131C,152C,159C,166C,190C,203C,226C,258C,306C,310C,338C,345C 'fileserv':276C 'fileserver-match-trailing-slash':275C 'fix':227C 'follow':336C 'frankenphp':115C 'get':366C 'glob':186C 'impact':340C 'keep':228C,268C 'localhost':315C,324C 'longer':254C 'match':101C,202C,255C,262C,277C 'matcher':86C,132C,175C 'meant':357C 'modules/caddyhttp/fileserver/matcher.go':133C 'name':191C 'noth':365C 'origin':214C 'oss':284C 'patch':270C 'path':76C,85C,93C,160C,174C,185C,194C,215C,220C,267C,299C,307C,308C,321C,354C 'path-as-i':320C 'path.base':211C 'path.clean':135C,237C 'pattern':153C 'php':112C,117C,334C 'place':95C 'pr':272C 'prefix':360C 'proof':291C 'public':271C 'read':343C 'redirect':111C,209C 'regexp':195C 'regular':81C,165C,257C 'request':171C,180C 'requir':240C 'resolv':77C,161C 'respond':302C 'restor':146C 'rewrit':98C,169C,199C 'rewritten':219C 'root':296C 'rout':128C,301C 'rule':90C,183C,355C,361C 'run':134C 'scanner':285C 'see':87C,176C 'serv':105C,224C 'server':104C,118C,204C,311C,339C 'skip':206C 'slash':144C,231C,279C 'split':266C 'static':344C 'still':261C,363C 'strictfileexist':238C 'suffix':182C,351C 'summari':73C 'trail':143C,230C,278C 'tri':74C,126C,151C,158C,305C 'unauthent':341C 'vagwnyyz':290C 'vulner':287C 'without':108C 'work':269C,332C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-04T00:59:36+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-25cw-38mq-q7m6","url":"https://github.com/caddyserver/caddy/security/advisories/GHSA-25cw-38mq-q7m6","severity":"medium","published_at":"2026-11-04T00:59:36+00:00","modified_at":"2026-11-04T01:01:57+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":110735,"url":"https://github.com/caddyserver/caddy/security/advisories/GHSA-25cw-38mq-q7m6","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"}