fastcgi: rewrite to index.php only if it exists (#8372)

php_fastcgi used first_exist_fallback, which since v2.11.8 matches its final entry even when the file is absent. On a site without index.php, "/", directories with an index.html and missing files were all rewritten to the missing index.php, so file_server never served them.

php_fastcgi now uses first_exist, as v2.11.7 effectively did since globbing skipped absent files. Explicitly configured first_exist_fallback keeps its semantics. The transport normalizes the path itself, so the dot-segment protection doesn't depend on this rewrite.

Fixes #8367
This commit is contained in:
Kévin Dunglas authored and GitHub committed 2026-11-08 22:04:20 +00:00
1 parent b30c019341
commit 8822b60863
6 files changed
+52 -15

No files matched your search

@@ -58,7 +58,6 @@
"{http.request.uri.path}/index.php",
"index.php"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php"
]
@@ -73,8 +73,7 @@ php_fastcgi @test localhost:9000
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
],
"try_policy": "first_exist_fallback"
]
}
}
]
@@ -59,7 +59,6 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}/index.php5",
"index.php5"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php",
".php5"
@@ -32,7 +32,6 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}",
"index.php"
],
"try_policy": "first_exist_fallback",
"split_path": [
".php",
".php5"
+50
View File
@@ -227,3 +227,53 @@ func TestPHPFastCGIDirectoryRedirectStaysOnOrigin(t *testing.T) {
}
}
}
// Without a PHP front controller, php_fastcgi must leave static files and
// directory indexes to file_server instead of sending them to a missing
// index.php (https://github.com/caddyserver/caddy/issues/8367).
func TestPHPFastCGIWithoutIndexPHP(t *testing.T) {
root := t.TempDir()
for name, content := range map[string]string{
"index.html": "home",
"style.css": "css",
"about/index.html": "about",
} {
file := filepath.Join(root, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
backend := startFakePHPFPM(t)
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost:9080 {
root * %s
php_fastcgi %s
file_server
}
`, root, backend), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", http.StatusOK, "home")
tester.AssertGetResponse("http://localhost:9080/about/", http.StatusOK, "about")
tester.AssertGetResponse("http://localhost:9080/style.css", http.StatusOK, "css")
tester.AssertGetResponse("http://localhost:9080/missing", http.StatusNotFound, "")
// a front controller added later handles the paths that match no file
if err := os.WriteFile(filepath.Join(root, "index.php"), []byte("<?php // front controller"), 0o644); err != nil {
t.Fatal(err)
}
tester.AssertGetResponse("http://localhost:9080/missing", http.StatusOK, "SCRIPT_FILENAME="+filepath.Join(root, "index.php"))
tester.AssertGetResponse("http://localhost:9080/style.css", http.StatusOK, "css")
}
@@ -19,7 +19,6 @@ import (
"net/http"
"slices"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -140,10 +139,9 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// }
// redir @canonicalPath {path}/ 308
//
// # If the requested file does not exist, try index files and assume index.php always exists
// # If the requested file does not exist, try index files
// @indexFiles file {
// try_files {path} {path}/index.php index.php
// try_policy first_exist_fallback
// split_path .php
// }
// rewrite @indexFiles {http.matchers.file.relative}
@@ -317,18 +315,12 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
if indexFile != "off" {
var dirRedir bool
dirIndex := "{http.request.uri.path}/" + indexFile
tryPolicy := "first_exist_fallback"
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
dirRedir = true
} else {
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
// use first_exist strategy if the last file is not a PHP file
tryPolicy = ""
}
dirRedir = slices.Contains(tryFiles, dirIndex)
}
@@ -370,7 +362,6 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
TryPolicy: tryPolicy,
SplitPath: extensions,
}),
}