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