From 0917068048eca8316b5c066e00613d002566beaa Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Mon, 19 Apr 2021 00:40:51 +0200 Subject: [PATCH 1/3] begin of work on pseudo-sandbox mode --- internal/runner/options.go | 4 +++ internal/runner/runner.go | 2 ++ pkg/httpserver/httpserver.go | 22 +++++++++++++-- pkg/httpserver/loglayer.go | 44 ++++++++++++++++++++++++++++- pkg/httpserver/sandboxfs.go | 55 ++++++++++++++++++++++++++++++++++++ pkg/httpserver/util.go | 5 ++++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 pkg/httpserver/sandboxfs.go create mode 100644 pkg/httpserver/util.go diff --git a/internal/runner/options.go b/internal/runner/options.go index bf69db3..78f8579 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -29,6 +29,8 @@ type Options struct { TCPWithTLS bool Version bool Silent bool + Sandbox bool + MaxFileSize int } // ParseOptions parses the command line options for application @@ -49,6 +51,8 @@ func ParseOptions() *Options { flag.StringVar(&options.Realm, "realm", "Please enter username and password", "Realm") flag.BoolVar(&options.Version, "version", false, "Show version of the software") flag.BoolVar(&options.Silent, "silent", false, "Show only results in the output") + flag.BoolVar(&options.Sandbox, "sandbox", false, "Enable sandbox mode") + flag.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size") flag.Parse() diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 7d69e25..5806044 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -57,6 +57,8 @@ func New(options *Options) (*Runner, error) { BasicAuthPassword: r.options.password, BasicAuthReal: r.options.Realm, Verbose: r.options.Verbose, + Sandbox: r.options.Sandbox, + MaxFileSize: r.options.MaxFileSize, }) if err != nil { return nil, err diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go index d516caf..72da466 100644 --- a/pkg/httpserver/httpserver.go +++ b/pkg/httpserver/httpserver.go @@ -1,7 +1,10 @@ package httpserver import ( + "errors" "net/http" + "os" + "path/filepath" "github.com/projectdiscovery/sslcert" ) @@ -19,6 +22,8 @@ type Options struct { BasicAuthPassword string BasicAuthReal string Verbose bool + Sandbox bool + MaxFileSize int // 50Mb } // HTTPServer instance @@ -32,9 +37,22 @@ func New(options *Options) (*HTTPServer, error) { var h HTTPServer EnableUpload = options.EnableUpload EnableVerbose = options.Verbose - h.layers = h.loglayer(http.FileServer(http.Dir(options.Folder))) + folder, err := filepath.Abs(options.Folder) + if err != nil { + return nil, err + } + if _, err := os.Stat(folder); os.IsNotExist(err) { + return nil, errors.New("path does not exist") + } + options.Folder = folder + var dir http.FileSystem + dir = http.Dir(options.Folder) + if options.Sandbox { + dir = SandboxFileSystem{fs: http.Dir(options.Folder), RootFolder: options.Folder} + } + h.layers = h.loglayer(http.FileServer(dir)) if options.BasicAuthUsername != "" || options.BasicAuthPassword != "" { - h.layers = h.loglayer(h.basicauthlayer(http.FileServer(http.Dir(options.Folder)))) + h.layers = h.loglayer(h.basicauthlayer(http.FileServer(dir))) } h.options = options diff --git a/pkg/httpserver/loglayer.go b/pkg/httpserver/loglayer.go index 1e64b8f..75f4ba6 100644 --- a/pkg/httpserver/loglayer.go +++ b/pkg/httpserver/loglayer.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httputil" "path" + "path/filepath" "github.com/projectdiscovery/gologger" ) @@ -24,13 +25,54 @@ func (t *HTTPServer) loglayer(handler http.Handler) http.Handler { // Handles file write if enabled if EnableUpload && r.Method == http.MethodPut { - data, err := ioutil.ReadAll(r.Body) + // sandbox - calcolate absolute path + if t.options.Sandbox { + absPath, err := filepath.Abs(filepath.Join(t.options.Folder, r.URL.Path)) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + // check if the path is within the configured folder + pattern := t.options.Folder + string(filepath.Separator) + "*" + matched, err := filepath.Match(pattern, absPath) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } else if !matched { + gologger.Print().Msg("pointing to unauthorized directory") + w.WriteHeader(http.StatusBadRequest) + return + } + } + + var ( + data []byte + err error + ) + if t.options.Sandbox { + maxFileSize := toMb(t.options.MaxFileSize) + // check header content length + if r.ContentLength > maxFileSize { + gologger.Print().Msg("request too large") + return + } + // body max length + r.Body = http.MaxBytesReader(w, r.Body, maxFileSize) + } + + data, err = ioutil.ReadAll(r.Body) if err != nil { gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return } err = handleUpload(path.Base(r.URL.Path), data) if err != nil { gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return } } diff --git a/pkg/httpserver/sandboxfs.go b/pkg/httpserver/sandboxfs.go new file mode 100644 index 0000000..41fcadf --- /dev/null +++ b/pkg/httpserver/sandboxfs.go @@ -0,0 +1,55 @@ +package httpserver + +import ( + "errors" + "net/http" + "path/filepath" +) + +type SandboxFileSystem struct { + fs http.FileSystem + RootFolder string +} + +func (sbfs SandboxFileSystem) Open(path string) (http.File, error) { + abspath, err := filepath.Abs(filepath.Join(sbfs.RootFolder, path)) + if err != nil { + return nil, err + } + + filename := filepath.Base(abspath) + // rejects names starting with a dot like .file + dotmatch, err := filepath.Match(".*", filename) + if err != nil { + return nil, err + } else if dotmatch { + return nil, errors.New("invalid file") + } + + // reject symlinks + symlinkCheck, err := filepath.EvalSymlinks(abspath) + if err != nil { + return nil, err + } + if symlinkCheck != abspath { + return nil, errors.New("symlinks not allowed") + } + + // check if the path is within the configured folder + if sbfs.RootFolder != abspath { + pattern := sbfs.RootFolder + string(filepath.Separator) + "*" + matched, err := filepath.Match(pattern, abspath) + if err != nil { + return nil, err + } else if !matched { + return nil, errors.New("invalid file") + } + } + + f, err := sbfs.fs.Open(path) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/pkg/httpserver/util.go b/pkg/httpserver/util.go new file mode 100644 index 0000000..4c69d6f --- /dev/null +++ b/pkg/httpserver/util.go @@ -0,0 +1,5 @@ +package httpserver + +func toMb(n int) int64 { + return int64(n) * 1024 * 1024 +} From a8c31d49cbf355a165b85725fbf631b1bd46f978 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Mon, 19 Apr 2021 00:53:04 +0200 Subject: [PATCH 2/3] adding missing comments --- pkg/httpserver/sandboxfs.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/httpserver/sandboxfs.go b/pkg/httpserver/sandboxfs.go index 41fcadf..cde5c04 100644 --- a/pkg/httpserver/sandboxfs.go +++ b/pkg/httpserver/sandboxfs.go @@ -6,11 +6,13 @@ import ( "path/filepath" ) +// SandboxFileSystem implements superbasic security checks type SandboxFileSystem struct { fs http.FileSystem RootFolder string } +// Open performs basic security checks before providing folder/file content func (sbfs SandboxFileSystem) Open(path string) (http.File, error) { abspath, err := filepath.Abs(filepath.Join(sbfs.RootFolder, path)) if err != nil { From 74c20cef99513c7ef98b79b1155e64762bc70a09 Mon Sep 17 00:00:00 2001 From: mzack Date: Wed, 16 Jun 2021 18:23:52 +0200 Subject: [PATCH 3/3] adding additional checks --- internal/runner/options.go | 6 +++++- pkg/httpserver/loglayer.go | 2 +- pkg/httpserver/uploadlayer.go | 20 ++++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/runner/options.go b/internal/runner/options.go index 78f8579..a5869d6 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -40,7 +40,11 @@ func ParseOptions() *Options { flag.BoolVar(&options.EnableTCP, "tcp", false, "TCP Server") flag.BoolVar(&options.TCPWithTLS, "tls", false, "Enable TCP TLS") flag.StringVar(&options.RulesFile, "rules", "", "Rules yaml file") - flag.StringVar(&options.Folder, "path", ".", "Folder") + currentPath := "." + if p, err := os.Getwd(); err == nil { + currentPath = p + } + flag.StringVar(&options.Folder, "path", currentPath, "Folder") flag.BoolVar(&options.EnableUpload, "upload", false, "Enable upload via PUT") flag.BoolVar(&options.HTTPS, "https", false, "HTTPS") flag.StringVar(&options.TLSCertificate, "cert", "", "HTTPS Certificate") diff --git a/pkg/httpserver/loglayer.go b/pkg/httpserver/loglayer.go index 75f4ba6..0e1a87a 100644 --- a/pkg/httpserver/loglayer.go +++ b/pkg/httpserver/loglayer.go @@ -68,7 +68,7 @@ func (t *HTTPServer) loglayer(handler http.Handler) http.Handler { w.WriteHeader(http.StatusInternalServerError) return } - err = handleUpload(path.Base(r.URL.Path), data) + err = handleUpload(t.options.Folder, path.Base(r.URL.Path), data) if err != nil { gologger.Print().Msgf("%s\n", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/httpserver/uploadlayer.go b/pkg/httpserver/uploadlayer.go index 2663fba..928ac60 100644 --- a/pkg/httpserver/uploadlayer.go +++ b/pkg/httpserver/uploadlayer.go @@ -1,7 +1,23 @@ package httpserver -import "io/ioutil" +import ( + "errors" + "io/ioutil" + "path/filepath" + "strings" +) + +func handleUpload(base, file string, data []byte) error { + // rejects all paths containing a non exhaustive list of invalid characters - This is only a best effort as the tool is meant for development + if strings.ContainsAny(file, "\\`\"':") { + return errors.New("invalid character") + } + + // allow upload only in subfolders + rel, err := filepath.Rel(base, file) + if rel == "" || err != nil { + return err + } -func handleUpload(file string, data []byte) error { return ioutil.WriteFile(file, data, 0655) }