Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 03:30 UTC
fileserver: treat a path below a regular file as not found (#8131)
Since #7017, every *fs.PathError from stat becomes fs.ErrInvalid, and that includes ENOTDIR. So a request like /index.php/foo got 400 instead of 404, and pass_thru no longer fell through to the next handler. Map ENOTDIR to fs.ErrNotExist before that check. Other path errors, such as NUL bytes, overly long names, or transient I/O failures, keep their current handling.
klokv committed
Oct 4, 2026 at 15:13 UTC
7ba5e147acb504c825081d0640e14bd021ecbb00
2 files changed
+55
modules/caddyhttp/fileserver/staticfiles.go
+6
@@ -29,6 +29,7 @@ import (
29
"runtime"
30
"strconv"
31
"strings"
32
+ "syscall"
33
"time"
34
"unicode/utf8"
35
@@ -689,6 +690,11 @@ func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, nam
690
return originalErr
691
}
692
693
+ // a regular file used as a directory, e.g. /index.php/foo
694
+ if errors.Is(originalErr, syscall.ENOTDIR) {
695
+ return fs.ErrNotExist
696
+ }
697
+
698
if _, ok := errors.AsType[*fs.PathError](originalErr); ok {
699
return fs.ErrInvalid
700
}
modules/caddyhttp/fileserver/staticfiles_test.go
+49
@@ -18,6 +18,7 @@ import (
18
"bytes"
19
"compress/gzip"
20
"context"
21
+ "errors"
22
"fmt"
23
"net/http"
24
"net/http/httptest"
@@ -29,6 +30,7 @@ import (
30
"time"
31
32
"github.com/caddyserver/caddy/v2"
33
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
34
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
35
)
36
@@ -440,3 +442,50 @@ func (p testPrecompressed) AcceptEncoding() string {
442
func (p testPrecompressed) Suffix() string {
443
return p.suffix
444
}
445
+
446
+// A path below a regular file does not exist, so it is a 404 that pass_thru
447
+// falls through, while a path containing a NUL byte is malformed and is a 400.
448
+func TestServeHTTPStatErrors(t *testing.T) {
449
+ root := t.TempDir()
450
+ if err := os.WriteFile(filepath.Join(root, "file.txt"), nil, 0o600); err != nil {
451
+ t.Fatal(err)
452
+ }
453
+
454
+ fsrv := FileServer{Root: root}
455
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
456
+ defer cancel()
457
+ if err := fsrv.Provision(ctx); err != nil {
458
+ t.Fatal(err)
459
+ }
460
+
461
+ for i, tc := range []struct {
462
+ path string
463
+ passThru bool
464
+ wantStatus int
465
+ wantNext bool
466
+ }{
467
+ {path: "/file.txt/child", wantStatus: http.StatusNotFound},
468
+ {path: "/file.txt/child", passThru: true, wantNext: true},
469
+ {path: "/file.txt%00", wantStatus: http.StatusBadRequest},
470
+ } {
471
+ fsrv.PassThru = tc.passThru
472
+ r := httptest.NewRequest(http.MethodGet, tc.path, nil)
473
+ r = r.WithContext(context.WithValue(r.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer()))
474
+ var calledNext bool
475
+ next := caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
476
+ calledNext = true
477
+ return nil
478
+ })
479
+
480
+ err := fsrv.ServeHTTP(httptest.NewRecorder(), r, next)
481
+ if tc.wantNext {
482
+ if err != nil || !calledNext {
483
+ t.Errorf("Test %d: expected the next handler, got called=%t err=%v", i, calledNext, err)
484
+ }
485
+ continue
486
+ }
487
+ if he, ok := errors.AsType[caddyhttp.HandlerError](err); !ok || he.StatusCode != tc.wantStatus {
488
+ t.Errorf("Test %d: expected status %d, got %v", i, tc.wantStatus, err)
489
+ }
490
+ }
491
+}