answer 404 for a static path the root refuses
Found by /invariants: a symlink under static/ pointing outside the site root answered 500. The guard held — os.Root refused it and no bytes escaped — but the response confirmed the path was there, where every other miss answers 404. Same reasoning as a hidden bundle answering 404 rather than 403 (ADR-0024). serveStatic now stats through the rooted FS first, so a directory, a missing file, and a refused name are one answer. That also folds the old noListing and staticFS into one function, since "cannot serve this" was already their shared job. The test uses a real temp directory rather than a MapFS, because the guard under test belongs to os.Root; verified it fails with 500 against the previous code before keeping it. Splitting web_test.go at the seam the package already had — resolve_test.go for what a path means, web_test.go for what happens once it resolves — because it crossed FILE_LOC_WARN. Same response as content.go at entry 9.
This commit is contained in:
+13
-14
@@ -21,29 +21,28 @@ func Handler(site *content.Site, r *render.Renderer, siteFS fs.FS) http.Handler
|
||||
serve(w, req, site, r)
|
||||
})
|
||||
if siteFS != nil {
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", noListing(staticFS(siteFS))))
|
||||
if sub, err := fs.Sub(siteFS, "static"); err == nil {
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", serveStatic(sub)))
|
||||
}
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
// staticFS serves the site root's static/ directory verbatim. It keeps the os.Root guarantee, because
|
||||
// serveStatic serves the site root's static/ directory verbatim. It keeps the os.Root guarantee, because
|
||||
// the fs.FS it is given is the one rooted there (ADR-0031).
|
||||
func staticFS(siteFS fs.FS) http.Handler {
|
||||
sub, err := fs.Sub(siteFS, "static")
|
||||
if err != nil {
|
||||
return http.NotFoundHandler()
|
||||
}
|
||||
return http.FileServerFS(sub)
|
||||
}
|
||||
|
||||
// noListing refuses directory paths, so static/ never answers with an index of its own contents.
|
||||
func noListing(h http.Handler) http.Handler {
|
||||
//
|
||||
// Anything it cannot serve answers 404: a directory, a missing file, or a name the root refuses because it
|
||||
// resolves outside. A listing would expose the tree, and an error page for a refused symlink would confirm
|
||||
// the path is there — the same reason a hidden bundle answers 404 rather than 403 (ADR-0024).
|
||||
func serveStatic(sub fs.FS) http.Handler {
|
||||
files := http.FileServerFS(sub)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path == "" || strings.HasSuffix(req.URL.Path, "/") {
|
||||
info, err := fs.Stat(sub, strings.TrimPrefix(req.URL.Path, "/"))
|
||||
if err != nil || info.IsDir() {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, req)
|
||||
files.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user