@marcel91200 / caddy Mirror · read-only / commits / b30c0193
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 10, 23:45 UTC

fastcgi: split script path at segment boundary (#8386)

The transport split the path at the first split extension anywhere, so "/admin.php.php" and "/admin.phpx/y.php" ran "/admin.php" with the rest as PATH_INFO. Route matchers judged a different file: "path /admin.php /admin.php/*" didn't match, and with php_fastcgi the file matcher rewrote "/a.phpx/b.php" to that file while PHP ran "/a.php". With "index off", or try_files not ending in a PHP file, nothing rewrites the request, so the protected script runs. A match must now end the path or be followed by "/", as in the file matcher.

Kévin Dunglas committed Nov 8, 2026 at 22:54 UTC b30c01934116ab2d0f564276a8af4b0e05d190b6
2 files changed +28 -6
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+6 -2
@@ -487,6 +487,10 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
487 // example: if splitPath is [".php"]
488 // "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
489 //
490 +// A match must end the path or be followed by "/", as in the file matcher, so
491 +// "/admin.php.php" or "/admin.phpx/y.php" can't run "/admin.php" past route
492 +// matchers that judged another file.
493 +//
494 // Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path
495 // never match any split entry: split strings are validated ASCII-only and
496 // lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or
@@ -533,8 +537,8 @@ func (t Transport) splitPos(path string) int {
537 }
538 }
539
536 - if match {
537 - return i + splitLen
540 + if end := i + splitLen; match && (end == pathLen || path[end] == '/') {
541 + return end
542 }
543 }
544 }
modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go
+22 -4
@@ -148,7 +148,7 @@ func TestSplitPos(t *testing.T) {
148 },
149 {
150 name: "unicode in filename with multiple php occurrences",
151 - path: "/ȺȺȺȺshell.php.txt.php",
151 + path: "/ȺȺȺȺshell.php/txt.php",
152 splitPath: []string{".php"},
153 wantPos: 18, // should match first .php, not be confused by byte offset shift
154 },
@@ -195,7 +195,25 @@ func TestSplitPos(t *testing.T) {
195 name: "extension in middle of filename",
196 path: "/test.php.bak",
197 splitPath: []string{".php"},
198 - wantPos: 9,
198 + wantPos: -1,
199 + },
200 + {
201 + name: "extension in middle of filename before a later script",
202 + path: "/admin.php.php",
203 + splitPath: []string{".php"},
204 + wantPos: 14,
205 + },
206 + {
207 + name: "extension in middle of a directory name",
208 + path: "/admin.phpx/y.php",
209 + splitPath: []string{".php"},
210 + wantPos: 17,
211 + },
212 + {
213 + name: "extension in middle of a directory name with path info",
214 + path: "/a.phpx/b.php/info",
215 + splitPath: []string{".php"},
216 + wantPos: 13,
217 },
218 // Regression tests adapted from FrankenPHP advisories
219 // GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase
@@ -290,7 +308,7 @@ func TestSplitPos(t *testing.T) {
308 // incorrect SCRIPT_NAME/PATH_INFO splitting
309 func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
310 // U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes.
293 - path := "/ȺȺȺȺshell.php.txt.php"
311 + path := "/ȺȺȺȺshell.php/txt.php"
312 split := []string{".php"}
313
314 pos := Transport{SplitPath: split}.splitPos(path)
@@ -306,7 +324,7 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
324 pathInfo := path[pos:]
325
326 assert.Equal(t, "/ȺȺȺȺshell.php", scriptName, "script name should be the path up to first .php")
309 - assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
327 + assert.Equal(t, "/txt.php", pathInfo, "path info should be the remainder after first .php")
328 }
329 }
330