From 4affa492ad849a7a9940f2cb477d8e0ed5e61164 Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Sat, 31 Oct 2026 10:01:13 +0300 Subject: [PATCH 1/5] perf: avoid copying already rooted request paths --- cgi.go | 6 +++++- cgi_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/cgi.go b/cgi.go index 064bc14ac8..bb8d044b6a 100644 --- a/cgi.go +++ b/cgi.go @@ -345,7 +345,11 @@ func sanitizedPathJoin(root, reqPath string) string { // Clean with "path", not "filepath": on Windows filepath.Clean keeps the // ".." behind a leading "//" (UNC prefix), letting reqPath escape root. - relPath := path.Clean("/" + reqPath)[1:] + rootedPath := reqPath + if !strings.HasPrefix(rootedPath, "/") { + rootedPath = "/" + rootedPath + } + relPath := path.Clean(rootedPath)[1:] // A cleaned rooted path is always local on POSIX: only Windows needs the check if runtime.GOOS == "windows" && relPath != "" && !filepath.IsLocal(relPath) { // path is unsafe (see https://github.com/golang/go/issues/56336#issuecomment-1416214885) diff --git a/cgi_test.go b/cgi_test.go index 6675dd19e4..d8080238d7 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -50,6 +50,9 @@ func TestSanitizedPathJoin(t *testing.T) { expected string }{ {"leading slash", "/index.php", filepath.Join(root, "index.php")}, + {"multiple leading slashes", "///index.php", filepath.Join(root, "index.php")}, + {"multiple root slashes", "//", filepath.Clean(root) + separator}, + {"rooted nested traversal", "//foo/../../bar/", filepath.Join(root, "bar") + separator}, {"no leading slash", "index.php", filepath.Join(root, "index.php")}, {"nested", "/foo/bar/index.php", filepath.Join(root, "foo", "bar", "index.php")}, {"empty req path returns root", "", filepath.Clean(root)}, @@ -617,3 +620,25 @@ func BenchmarkSplitRawHeader(b *testing.B) { }) } } + +var sanitizedPathJoinResult string + +func BenchmarkSanitizedPathJoin(b *testing.B) { + for _, tc := range []struct { + name string + path string + }{ + {"index", "/index.php"}, + {"nested", "/products/catalog/index.php"}, + {"long", "/" + strings.Repeat("products/", 16) + "index.php"}, + {"relative", "index.php"}, + {"traversal", "/foo/../../etc/passwd"}, + } { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sanitizedPathJoinResult = sanitizedPathJoin("/srv/app/public", tc.path) + } + }) + } +} From 9f4eb4c4ff3dc2c82b09d6268153ddb25785212c Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Mon, 2 Nov 2026 08:50:46 +0300 Subject: [PATCH 2/5] fix: restore os import and reuse leading slash helper --- cgi.go | 6 +----- cgi_test.go | 4 ++++ frankenphp.go | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cgi.go b/cgi.go index bb8d044b6a..c5a10f4340 100644 --- a/cgi.go +++ b/cgi.go @@ -345,11 +345,7 @@ func sanitizedPathJoin(root, reqPath string) string { // Clean with "path", not "filepath": on Windows filepath.Clean keeps the // ".." behind a leading "//" (UNC prefix), letting reqPath escape root. - rootedPath := reqPath - if !strings.HasPrefix(rootedPath, "/") { - rootedPath = "/" + rootedPath - } - relPath := path.Clean(rootedPath)[1:] + relPath := path.Clean(ensureLeadingSlash(reqPath))[1:] // A cleaned rooted path is always local on POSIX: only Windows needs the check if runtime.GOOS == "windows" && relPath != "" && !filepath.IsLocal(relPath) { // path is unsafe (see https://github.com/golang/go/issues/56336#issuecomment-1416214885) diff --git a/cgi_test.go b/cgi_test.go index d8080238d7..9d3e7d139a 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -633,6 +633,10 @@ func BenchmarkSanitizedPathJoin(b *testing.B) { {"long", "/" + strings.Repeat("products/", 16) + "index.php"}, {"relative", "index.php"}, {"traversal", "/foo/../../etc/passwd"}, + {"repeated_slashes", strings.Repeat("/", 4096) + "index.php"}, + {"dot_segments", "/" + strings.Repeat("a/../", 512) + "index.php"}, + {"relative_dot_segments", strings.Repeat("a/../", 512) + "index.php"}, + {"long_segment", "/" + strings.Repeat("a", 4096) + ".php"}, } { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() diff --git a/frankenphp.go b/frankenphp.go index cd86c4981d..9eccf2ed48 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -26,6 +26,7 @@ import ( "fmt" "log/slog" "net/http" + "os" "os/signal" "runtime" "strings" From 037f4d8ec59e82c38b09b15c339c0c9ac1ea253d Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Mon, 2 Nov 2026 10:32:29 +0300 Subject: [PATCH 3/5] test: trim adversarial path benchmark cases --- cgi_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cgi_test.go b/cgi_test.go index 9d3e7d139a..92ecb76ea0 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -635,8 +635,6 @@ func BenchmarkSanitizedPathJoin(b *testing.B) { {"traversal", "/foo/../../etc/passwd"}, {"repeated_slashes", strings.Repeat("/", 4096) + "index.php"}, {"dot_segments", "/" + strings.Repeat("a/../", 512) + "index.php"}, - {"relative_dot_segments", strings.Repeat("a/../", 512) + "index.php"}, - {"long_segment", "/" + strings.Repeat("a", 4096) + ".php"}, } { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() From 0d90c4ad302b9c0f015e29697387ca5bb29a79d2 Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Mon, 2 Nov 2026 10:34:43 +0300 Subject: [PATCH 4/5] chore: drop os import from path optimization PR --- frankenphp.go | 1 - 1 file changed, 1 deletion(-) diff --git a/frankenphp.go b/frankenphp.go index 9eccf2ed48..cd86c4981d 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -26,7 +26,6 @@ import ( "fmt" "log/slog" "net/http" - "os" "os/signal" "runtime" "strings" From e72cb066332f36506629f5b5a95219d32330f1c1 Mon Sep 17 00:00:00 2001 From: "a.bolonin" Date: Mon, 2 Nov 2026 10:59:54 +0300 Subject: [PATCH 5/5] test: remove sanitized path benchmark --- cgi_test.go | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/cgi_test.go b/cgi_test.go index 92ecb76ea0..e6c11d5b92 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -620,27 +620,3 @@ func BenchmarkSplitRawHeader(b *testing.B) { }) } } - -var sanitizedPathJoinResult string - -func BenchmarkSanitizedPathJoin(b *testing.B) { - for _, tc := range []struct { - name string - path string - }{ - {"index", "/index.php"}, - {"nested", "/products/catalog/index.php"}, - {"long", "/" + strings.Repeat("products/", 16) + "index.php"}, - {"relative", "index.php"}, - {"traversal", "/foo/../../etc/passwd"}, - {"repeated_slashes", strings.Repeat("/", 4096) + "index.php"}, - {"dot_segments", "/" + strings.Repeat("a/../", 512) + "index.php"}, - } { - b.Run(tc.name, func(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - sanitizedPathJoinResult = sanitizedPathJoin("/srv/app/public", tc.path) - } - }) - } -}