diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go index 5dd65a6..4806367 100644 --- a/pkg/httpserver/httpserver.go +++ b/pkg/httpserver/httpserver.go @@ -62,7 +62,10 @@ func New(options *Options) (*HTTPServer, error) { var httpHandler http.Handler if options.Python { - httpHandler = PythonStyle(dir.(http.Dir)) + // PythonStyle now accepts http.FileSystem so SandboxFileSystem is + // honoured when -sandbox is also set (previously a type assertion to + // http.Dir would panic at startup when Sandbox=true). + httpHandler = PythonStyle(dir) } else { httpHandler = http.FileServer(dir) } diff --git a/pkg/httpserver/httpserver_security_test.go b/pkg/httpserver/httpserver_security_test.go new file mode 100644 index 0000000..df83012 --- /dev/null +++ b/pkg/httpserver/httpserver_security_test.go @@ -0,0 +1,48 @@ +package httpserver + +import ( + "net/http" + "testing" +) + +// TestPythonStyleAcceptFileSystem verifies that PythonStyle accepts an +// http.FileSystem interface (not a concrete http.Dir). Before the fix, +// httpserver.New() panicked with +// +// interface conversion: http.FileSystem is httpserver.SandboxFileSystem, not http.Dir +// +// when both -sandbox and -python flags were set, because PythonStyle only +// accepted http.Dir via a type assertion. +func TestPythonStyleAcceptFileSystem(t *testing.T) { + // SandboxFileSystem implements http.FileSystem but is NOT http.Dir. + // Passing it to PythonStyle must not panic. + sbfs := SandboxFileSystem{ + fs: http.Dir(t.TempDir()), + RootFolder: t.TempDir(), + } + // This call would panic on the old code because of dir.(http.Dir). + handler := PythonStyle(sbfs) + if handler == nil { + t.Fatal("PythonStyle returned nil handler") + } +} + +// TestNewServerSandboxPythonNoPanic mirrors the exact code path in New() +// that used to panic: Sandbox=true + Python=true. +func TestNewServerSandboxPythonNoPanic(t *testing.T) { + dir := t.TempDir() + opts := &Options{ + Folder: dir, + ListenAddress: "127.0.0.1:0", + Sandbox: true, + Python: true, + } + // Before the fix this returned a panic, not an error. + srv, err := New(opts) + if err != nil { + t.Fatalf("New() returned unexpected error: %v", err) + } + if srv == nil { + t.Fatal("New() returned nil server") + } +} diff --git a/pkg/httpserver/pythonliststyle.go b/pkg/httpserver/pythonliststyle.go index ee6342f..fc12812 100644 --- a/pkg/httpserver/pythonliststyle.go +++ b/pkg/httpserver/pythonliststyle.go @@ -5,8 +5,6 @@ import ( "fmt" "io" "net/http" - "os" - "path/filepath" ) const ( @@ -30,7 +28,7 @@ const ( type pythonStyleHandler struct { origWriter http.ResponseWriter - root http.Dir + root http.FileSystem } func (h *pythonStyleHandler) Header() http.Header { @@ -75,22 +73,33 @@ func (h *pythonStyleHandler) WriteHeader(statusCode int) { } func (h *pythonStyleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - target := filepath.Join(string(h.root), filepath.Clean(request.URL.Path)) - file, err := os.Stat(target) + // Use the FileSystem's Open to respect sandbox containment checks. + // This avoids building raw OS paths and correctly delegates to SandboxFileSystem + // or any other http.FileSystem implementation. + f, err := h.root.Open(request.URL.Path) + if err != nil { + http.NotFound(writer, request) + return + } + defer f.Close() - if err != nil || !file.IsDir() { - http.ServeFile(writer, request, target) + fi, err := f.Stat() + if err != nil || !fi.IsDir() { + // Not a directory: serve through the FileSystem handler + http.FileServer(h.root).ServeHTTP(writer, request) return - } else { - _, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path) - _, _ = fmt.Fprintf(writer, "