// Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package integration import ( "fmt" "io" "net" "net/http" "net/http/fcgi" "os" "path/filepath" "runtime" "strings" "testing" "github.com/caddyserver/caddy/v2/caddytest" ) // startFakePHPFPM starts a FastCGI responder that reports the SCRIPT_FILENAME it // was asked to execute, so a test can assert which script Caddy selected. func startFakePHPFPM(t *testing.T) string { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listening: %v", err) } t.Cleanup(func() { ln.Close() }) go func() { _ = fcgi.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { env := fcgi.ProcessEnv(r) fmt.Fprintf(w, "SCRIPT_FILENAME=%s", env["SCRIPT_FILENAME"]) })) }() return ln.Addr().String() } // A php_fastcgi route protected by a path matcher must not execute a script the // matcher never evaluated. Path matchers judge the cleaned request path, so // /admin.php/../../other.php is matched as /other.php and must not cause // /admin.php to be executed. // // `index off` is used so that php_fastcgi generates no rewrite route at all // (fastcgi/caddyfile.go skips the redirect and try_files routes entirely). That // rewrite is otherwise the only step that normalizes r.URL.Path before the // transport reads it, so without this the test would be satisfied by the rewrite // rather than by the transport, and would not cover the transport's own // normalization. func TestPHPFastCGIProtectedRouteIgnoresDotSegments(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "admin.php"), []byte("/sub/index.php, but the Location header used to // be built from the raw original path and came out as //evil.com/../sub/, which // browsers treat as a scheme-relative URL to another host. The relative // reference is built from the escaped original segment, so names with reserved // or non-ASCII characters must still round-trip to the same directory. func TestPHPFastCGIDirectoryRedirectStaysOnOrigin(t *testing.T) { dirs := []string{"sub", "a b", "a#b", "50%", "a;b", "café", "x/sub"} if runtime.GOOS != "windows" { dirs = append(dirs, "a?b", "a:b") } root := t.TempDir() for _, dir := range dirs { if err := os.MkdirAll(filepath.Join(root, filepath.FromSlash(dir)), 0o755); err != nil { t.Fatalf("creating %s: %v", dir, err) } if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(dir), "index.php"), []byte(" 0x7e { t.Errorf("requesting %q: Location %q contains an unescaped byte %#x", tc.uri, loc, loc[i]) break } } } } // 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("