Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 20 additions & 2 deletions pkg/httpserver/httpserver.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package httpserver

import (
"errors"
"net/http"
"os"
"path/filepath"

"github.com/projectdiscovery/sslcert"
)
Expand All @@ -19,6 +22,8 @@ type Options struct {
BasicAuthPassword string
BasicAuthReal string
Verbose bool
Sandbox bool
MaxFileSize int // 50Mb
}

// HTTPServer instance
Expand All @@ -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

Expand Down
46 changes: 44 additions & 2 deletions pkg/httpserver/loglayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httputil"
"path"
"path/filepath"

"github.com/projectdiscovery/gologger"
)
Expand All @@ -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
}
}

Expand Down
57 changes: 57 additions & 0 deletions pkg/httpserver/sandboxfs.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 18 additions & 2 deletions pkg/httpserver/uploadlayer.go
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 5 additions & 0 deletions pkg/httpserver/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package httpserver

func toMb(n int) int64 {
return int64(n) * 1024 * 1024
}