// 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 } } } }