Skip to content

Commit

Permalink
Merge pull request projectdiscovery#35 from projectdiscovery/feature-…
Browse files Browse the repository at this point in the history
…sandbox

Adding pseudo-sandbox mode
  • Loading branch information
ehsandeep committed Jun 24, 2021
2 parents e20dd81 + 74c20ce commit 2ccf601
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 7 deletions.
10 changes: 9 additions & 1 deletion internal/runner/options.go
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
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
@@ -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
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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
package httpserver

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

0 comments on commit 2ccf601

Please sign in to comment.