net/http: http.FileServer returns 404 when a path is invalid or unsafe

This commit is contained in:
Grégoire Lodi 2025-03-05 10:12:19 +01:00
parent 350118666d
commit ed93ac5f29
2 changed files with 55 additions and 1 deletions

View File

@ -67,6 +67,11 @@ func mapOpenError(originalErr error, name string, sep rune, stat func(string) (f
return originalErr return originalErr
} }
// errInvalidUnsafePath is returned by Dir.Open when the call to
// filepath.Localize fails. filepath.Localize returns an error if the path
// cannot be represented by the operating system.
var errInvalidUnsafePath = errors.New("http: invalid or unsafe file path")
// Open implements [FileSystem] using [os.Open], opening files for reading rooted // Open implements [FileSystem] using [os.Open], opening files for reading rooted
// and relative to the directory d. // and relative to the directory d.
func (d Dir) Open(name string) (File, error) { func (d Dir) Open(name string) (File, error) {
@ -76,7 +81,7 @@ func (d Dir) Open(name string) (File, error) {
} }
path, err := filepath.Localize(path) path, err := filepath.Localize(path)
if err != nil { if err != nil {
return nil, errors.New("http: invalid or unsafe file path") return nil, errInvalidUnsafePath
} }
dir := string(d) dir := string(d)
if dir == "" { if dir == "" {
@ -768,6 +773,9 @@ func toHTTPError(err error) (msg string, httpStatus int) {
if errors.Is(err, fs.ErrPermission) { if errors.Is(err, fs.ErrPermission) {
return "403 Forbidden", StatusForbidden return "403 Forbidden", StatusForbidden
} }
if errors.Is(err, errInvalidUnsafePath) {
return "404 page not found", StatusNotFound
}
// Default: // Default:
return "500 Internal Server Error", StatusInternalServerError return "500 Internal Server Error", StatusInternalServerError
} }

View File

@ -234,6 +234,31 @@ func TestServeFile_DotDot(t *testing.T) {
} }
} }
func TestServeFile_InvalidUnsafePath(t *testing.T) {
tests := []struct {
req string
wantStatus int
}{
{"/testdata/file", 200},
{"/%00/file", 404},
{"/file%00", 404},
{"/%00", 404},
}
for _, tt := range tests {
req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
if err != nil {
t.Errorf("bad request %q: %v", tt.req, err)
continue
}
rec := httptest.NewRecorder()
ServeFile(rec, req, "testdata/file")
if rec.Code != tt.wantStatus {
t.Logf("%v", rec.Result())
t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
}
}
}
// Tests that this doesn't panic. (Issue 30165) // Tests that this doesn't panic. (Issue 30165)
func TestServeFileDirPanicEmptyPath(t *testing.T) { func TestServeFileDirPanicEmptyPath(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@ -733,6 +758,27 @@ func testFileServerZeroByte(t *testing.T, mode testMode) {
} }
} }
func TestFileServerNullByte(t *testing.T) { run(t, testFileServerNullByte) }
func testFileServerNullByte(t *testing.T, mode testMode) {
ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
for _, path := range []string{
"/file%00",
"/%00",
"/file/qwe/%00",
} {
res, err := ts.Client().Get(ts.URL + path)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != 404 {
t.Errorf("Get(%q): got status %v, want 404", path, res.StatusCode)
}
}
}
func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) } func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
func testFileServerNamesEscape(t *testing.T, mode testMode) { func testFileServerNamesEscape(t *testing.T, mode testMode) {
ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts