diff --git a/internal/runner/options.go b/internal/runner/options.go index bf69db3..a5869d6 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 @@ -38,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") @@ -49,6 +55,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..0e1a87a 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) + err = handleUpload(t.options.Folder, 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..cde5c04 --- /dev/null +++ b/pkg/httpserver/sandboxfs.go @@ -0,0 +1,57 @@ +package httpserver + +import ( + "errors" + "net/http" + "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 { + 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/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) } 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 +}