From dd6cc6919c19a260346f57fff8b407b21838e104 Mon Sep 17 00:00:00 2001 From: Neo Security Date: Fri, 13 Nov 2026 14:24:35 +0000 Subject: [PATCH] fix(security): fix sandbox bypass and panic when -sandbox and -python are both set PythonStyle previously accepted http.Dir via a type assertion: httpHandler = PythonStyle(dir.(http.Dir)) When -sandbox is also set, dir is a SandboxFileSystem, not http.Dir. The assertion panics at startup: interface conversion: http.FileSystem is httpserver.SandboxFileSystem, not http.Dir Beyond the crash, Python mode also bypassed SandboxFileSystem's containment checks (dot-file filter, symlink rejection, single-level glob) even when -sandbox was not set, because it built raw OS paths via filepath.Join+os.Stat instead of using the FileSystem's Open(). Fix: - Change PythonStyle to accept http.FileSystem (not http.Dir) - Replace os.Stat + raw filepath.Join path construction with fs.Open() so SandboxFileSystem's Open() is always consulted - Delegate actual file/directory serving to http.FileServer(root) which also uses the FileSystem Regression tests: TestPythonStyleAcceptFileSystem, TestNewServerSandboxPythonNoPanic --- pkg/httpserver/httpserver.go | 5 ++- pkg/httpserver/httpserver_security_test.go | 48 ++++++++++++++++++++++ pkg/httpserver/pythonliststyle.go | 37 ++++++++++------- 3 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 pkg/httpserver/httpserver_security_test.go 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, "

Directory listing for %s

\n
\n", request.URL.Path) - h.origWriter = writer - http.ServeFile(h, request, target) - _, _ = fmt.Fprint(writer, htmlFooter) } + _, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path) + _, _ = fmt.Fprintf(writer, "

Directory listing for %s

\n
\n", request.URL.Path) + h.origWriter = writer + http.FileServer(h.root).ServeHTTP(h, request) + _, _ = fmt.Fprint(writer, htmlFooter) } -func PythonStyle(root http.Dir) http.Handler { +// PythonStyle returns an HTTP handler that renders directory listings in Python's +// SimpleHTTPServer style. It accepts any http.FileSystem so that sandbox +// containment (SandboxFileSystem) is honoured even in this mode. +func PythonStyle(root http.FileSystem) http.Handler { return &pythonStyleHandler{ root: root, }