Skip to content

Commit 2ccf601

Browse files
authored
Merge pull request #35 from projectdiscovery/feature-sandbox
Adding pseudo-sandbox mode
2 parents e20dd81 + 74c20ce commit 2ccf601

File tree

7 files changed

+155
-7
lines changed

7 files changed

+155
-7
lines changed

internal/runner/options.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type Options struct {
2929
TCPWithTLS bool
3030
Version bool
3131
Silent bool
32+
Sandbox bool
33+
MaxFileSize int
3234
}
3335

3436
// ParseOptions parses the command line options for application
@@ -38,7 +40,11 @@ func ParseOptions() *Options {
3840
flag.BoolVar(&options.EnableTCP, "tcp", false, "TCP Server")
3941
flag.BoolVar(&options.TCPWithTLS, "tls", false, "Enable TCP TLS")
4042
flag.StringVar(&options.RulesFile, "rules", "", "Rules yaml file")
41-
flag.StringVar(&options.Folder, "path", ".", "Folder")
43+
currentPath := "."
44+
if p, err := os.Getwd(); err == nil {
45+
currentPath = p
46+
}
47+
flag.StringVar(&options.Folder, "path", currentPath, "Folder")
4248
flag.BoolVar(&options.EnableUpload, "upload", false, "Enable upload via PUT")
4349
flag.BoolVar(&options.HTTPS, "https", false, "HTTPS")
4450
flag.StringVar(&options.TLSCertificate, "cert", "", "HTTPS Certificate")
@@ -49,6 +55,8 @@ func ParseOptions() *Options {
4955
flag.StringVar(&options.Realm, "realm", "Please enter username and password", "Realm")
5056
flag.BoolVar(&options.Version, "version", false, "Show version of the software")
5157
flag.BoolVar(&options.Silent, "silent", false, "Show only results in the output")
58+
flag.BoolVar(&options.Sandbox, "sandbox", false, "Enable sandbox mode")
59+
flag.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size")
5260

5361
flag.Parse()
5462

internal/runner/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ func New(options *Options) (*Runner, error) {
5757
BasicAuthPassword: r.options.password,
5858
BasicAuthReal: r.options.Realm,
5959
Verbose: r.options.Verbose,
60+
Sandbox: r.options.Sandbox,
61+
MaxFileSize: r.options.MaxFileSize,
6062
})
6163
if err != nil {
6264
return nil, err

pkg/httpserver/httpserver.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package httpserver
22

33
import (
4+
"errors"
45
"net/http"
6+
"os"
7+
"path/filepath"
58

69
"github.com/projectdiscovery/sslcert"
710
)
@@ -19,6 +22,8 @@ type Options struct {
1922
BasicAuthPassword string
2023
BasicAuthReal string
2124
Verbose bool
25+
Sandbox bool
26+
MaxFileSize int // 50Mb
2227
}
2328

2429
// HTTPServer instance
@@ -32,9 +37,22 @@ func New(options *Options) (*HTTPServer, error) {
3237
var h HTTPServer
3338
EnableUpload = options.EnableUpload
3439
EnableVerbose = options.Verbose
35-
h.layers = h.loglayer(http.FileServer(http.Dir(options.Folder)))
40+
folder, err := filepath.Abs(options.Folder)
41+
if err != nil {
42+
return nil, err
43+
}
44+
if _, err := os.Stat(folder); os.IsNotExist(err) {
45+
return nil, errors.New("path does not exist")
46+
}
47+
options.Folder = folder
48+
var dir http.FileSystem
49+
dir = http.Dir(options.Folder)
50+
if options.Sandbox {
51+
dir = SandboxFileSystem{fs: http.Dir(options.Folder), RootFolder: options.Folder}
52+
}
53+
h.layers = h.loglayer(http.FileServer(dir))
3654
if options.BasicAuthUsername != "" || options.BasicAuthPassword != "" {
37-
h.layers = h.loglayer(h.basicauthlayer(http.FileServer(http.Dir(options.Folder))))
55+
h.layers = h.loglayer(h.basicauthlayer(http.FileServer(dir)))
3856
}
3957
h.options = options
4058

pkg/httpserver/loglayer.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httputil"
88
"path"
9+
"path/filepath"
910

1011
"github.com/projectdiscovery/gologger"
1112
)
@@ -24,13 +25,54 @@ func (t *HTTPServer) loglayer(handler http.Handler) http.Handler {
2425

2526
// Handles file write if enabled
2627
if EnableUpload && r.Method == http.MethodPut {
27-
data, err := ioutil.ReadAll(r.Body)
28+
// sandbox - calcolate absolute path
29+
if t.options.Sandbox {
30+
absPath, err := filepath.Abs(filepath.Join(t.options.Folder, r.URL.Path))
31+
if err != nil {
32+
gologger.Print().Msgf("%s\n", err)
33+
w.WriteHeader(http.StatusBadRequest)
34+
return
35+
}
36+
// check if the path is within the configured folder
37+
pattern := t.options.Folder + string(filepath.Separator) + "*"
38+
matched, err := filepath.Match(pattern, absPath)
39+
if err != nil {
40+
gologger.Print().Msgf("%s\n", err)
41+
w.WriteHeader(http.StatusBadRequest)
42+
return
43+
} else if !matched {
44+
gologger.Print().Msg("pointing to unauthorized directory")
45+
w.WriteHeader(http.StatusBadRequest)
46+
return
47+
}
48+
}
49+
50+
var (
51+
data []byte
52+
err error
53+
)
54+
if t.options.Sandbox {
55+
maxFileSize := toMb(t.options.MaxFileSize)
56+
// check header content length
57+
if r.ContentLength > maxFileSize {
58+
gologger.Print().Msg("request too large")
59+
return
60+
}
61+
// body max length
62+
r.Body = http.MaxBytesReader(w, r.Body, maxFileSize)
63+
}
64+
65+
data, err = ioutil.ReadAll(r.Body)
2866
if err != nil {
2967
gologger.Print().Msgf("%s\n", err)
68+
w.WriteHeader(http.StatusInternalServerError)
69+
return
3070
}
31-
err = handleUpload(path.Base(r.URL.Path), data)
71+
err = handleUpload(t.options.Folder, path.Base(r.URL.Path), data)
3272
if err != nil {
3373
gologger.Print().Msgf("%s\n", err)
74+
w.WriteHeader(http.StatusInternalServerError)
75+
return
3476
}
3577
}
3678

pkg/httpserver/sandboxfs.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package httpserver
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"path/filepath"
7+
)
8+
9+
// SandboxFileSystem implements superbasic security checks
10+
type SandboxFileSystem struct {
11+
fs http.FileSystem
12+
RootFolder string
13+
}
14+
15+
// Open performs basic security checks before providing folder/file content
16+
func (sbfs SandboxFileSystem) Open(path string) (http.File, error) {
17+
abspath, err := filepath.Abs(filepath.Join(sbfs.RootFolder, path))
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
filename := filepath.Base(abspath)
23+
// rejects names starting with a dot like .file
24+
dotmatch, err := filepath.Match(".*", filename)
25+
if err != nil {
26+
return nil, err
27+
} else if dotmatch {
28+
return nil, errors.New("invalid file")
29+
}
30+
31+
// reject symlinks
32+
symlinkCheck, err := filepath.EvalSymlinks(abspath)
33+
if err != nil {
34+
return nil, err
35+
}
36+
if symlinkCheck != abspath {
37+
return nil, errors.New("symlinks not allowed")
38+
}
39+
40+
// check if the path is within the configured folder
41+
if sbfs.RootFolder != abspath {
42+
pattern := sbfs.RootFolder + string(filepath.Separator) + "*"
43+
matched, err := filepath.Match(pattern, abspath)
44+
if err != nil {
45+
return nil, err
46+
} else if !matched {
47+
return nil, errors.New("invalid file")
48+
}
49+
}
50+
51+
f, err := sbfs.fs.Open(path)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return f, nil
57+
}

pkg/httpserver/uploadlayer.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
package httpserver
22

3-
import "io/ioutil"
3+
import (
4+
"errors"
5+
"io/ioutil"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
func handleUpload(base, file string, data []byte) error {
11+
// rejects all paths containing a non exhaustive list of invalid characters - This is only a best effort as the tool is meant for development
12+
if strings.ContainsAny(file, "\\`\"':") {
13+
return errors.New("invalid character")
14+
}
15+
16+
// allow upload only in subfolders
17+
rel, err := filepath.Rel(base, file)
18+
if rel == "" || err != nil {
19+
return err
20+
}
421

5-
func handleUpload(file string, data []byte) error {
622
return ioutil.WriteFile(file, data, 0655)
723
}

pkg/httpserver/util.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package httpserver
2+
3+
func toMb(n int) int64 {
4+
return int64(n) * 1024 * 1024
5+
}

0 commit comments

Comments
 (0)