@@ -6,15 +6,11 @@ import (
66 "io/fs"
77 "mime"
88 "net/http"
9- "net/url "
9+ "os "
1010 "path"
1111 "path/filepath"
12- "regexp"
13- "strings"
14- "unicode/utf8"
1512
1613 "golang.org/x/net/html"
17- "golang.org/x/text/unicode/norm"
1814)
1915
2016type fileServer struct {
@@ -26,103 +22,113 @@ func FileServer(fsys fs.FS) http.Handler {
2622 return & fileServer {http .FS (fsys )}
2723}
2824
29- // IsSafePath validates if a path is safe from path traversal attacks
30- func IsSafePath (p string ) bool {
31- var dangerousRunes = []rune {
32- '\uFF0F' , // fullwidth slash /
33- '\u2215' , // division slash ∕
34- '\u29F8' , // big solidus ⧸
35- '\u2044' , // fraction slash ⁄
36- '\\' , // backslash
37- '\uFF3C' , // fullwidth backslash \
38- '\uFE68' , // small reverse solidus ﹨
39- }
25+ func (f * fileServer ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
26+ path := path .Clean (path .Join ("/" , r .URL .Path ))
4027
41- // Recursive URL decode to prevent bypass via %252e%252e (double encoding)
42- path := p
43- for {
44- decoded , err := url .QueryUnescape (path )
45- if err != nil || decoded == path {
46- break
28+ serveIndex := func () {
29+ // not every fs contains a file named index.html,
30+ // therefore, we need to check if the file exists
31+ indexPath := "/index.html"
32+ file , err := f .fsys .Open (indexPath )
33+ if err != nil {
34+ http .NotFound (w , r )
35+ return
4736 }
48- path = decoded
49- }
50-
51- // Reject null byte injection
52- if strings .Contains (path , "\x00 " ) {
53- return false
54- }
55-
56- // Reject invalid UTF-8
57- if ! utf8 .ValidString (path ) {
58- return false
59- }
37+ defer file .Close ()
6038
61- // Reject dangerous runes
62- normalized := norm .NFC .String (path )
63- for _ , r := range normalized {
64- for _ , bad := range dangerousRunes {
65- if r == bad {
66- return false
67- }
39+ s , err := file .Stat ()
40+ if err != nil || s .IsDir () {
41+ http .NotFound (w , r )
42+ return
6843 }
69- }
70-
71- // match one or more /./ and /../
72- return ! regexp .MustCompile (`(?:[\/\\]\.+[\/\\])+` ).MatchString (path )
73- }
74-
75- func (f * fileServer ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
76- if ! IsSafePath (r .URL .Path ) {
77- http .NotFound (w , r )
78- return
79- }
8044
81- uPath := path . Clean ( path . Join ( "/ " , r . URL . Path ))
82- r . URL . Path = uPath
45+ w . Header (). Set ( "Content-Type " , mime . TypeByExtension ( filepath . Ext ( s . Name ()) ))
46+ w . Header (). Add ( "Vary" , "Accept-Encoding" )
8347
84- tryIndex := func () {
85- r .URL .Path = "/index.html"
86-
87- // not every fs contains a file named index.html,
88- // therefore, we need to check if the file exists and stop the recursion if it doesn't
89- file , err := f .fsys .Open (r .URL .Path )
90- if err != nil {
48+ buf := new (bytes.Buffer )
49+ w .Header ().Del ("Expires" )
50+ w .Header ().Set ("Cache-Control" , "no-cache" )
51+ if err := withBase (buf , file , "/" ); err != nil {
9152 http .NotFound (w , r )
9253 return
9354 }
94- defer file .Close ()
55+ if _ , err := w .Write (buf .Bytes ()); err != nil {
56+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
57+ return
58+ }
59+ }
9560
96- f .ServeHTTP (w , r )
61+ if ! isValid (f , path ) {
62+ serveIndex ()
63+ return
9764 }
9865
99- asset , err := f .fsys .Open (uPath )
66+ asset , err := f .fsys .Open (path )
10067 if err != nil {
101- tryIndex ()
68+ serveIndex ()
10269 return
10370 }
10471 defer asset .Close ()
10572
106- s , _ := asset .Stat ()
73+ s , err := asset .Stat ()
74+ if err != nil {
75+ serveIndex ()
76+ return
77+ }
10778 if s .IsDir () {
108- tryIndex ()
79+ serveIndex ()
10980 return
11081 }
11182
11283 w .Header ().Set ("Content-Type" , mime .TypeByExtension (filepath .Ext (s .Name ())))
84+ w .Header ().Add ("Vary" , "Accept-Encoding" )
11385
11486 buf := new (bytes.Buffer )
11587
11688 switch s .Name () {
11789 case "index.html" , "oidc-callback.html" , "oidc-silent-redirect.html" :
11890 w .Header ().Del ("Expires" )
11991 w .Header ().Set ("Cache-Control" , "no-cache" )
120- _ = withBase (buf , asset , "/" )
92+ err = withBase (buf , asset , "/" )
93+ if err != nil {
94+ http .NotFound (w , r )
95+ return
96+ }
12197 default :
122- _ , _ = buf .ReadFrom (asset )
98+ _ , err := buf .ReadFrom (asset )
99+ if err != nil {
100+ http .NotFound (w , r )
101+ return
102+ }
123103 }
124104
125- _ , _ = w .Write (buf .Bytes ())
105+ _ , err = w .Write (buf .Bytes ())
106+ if err != nil {
107+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
108+ return
109+ }
110+ }
111+
112+ func isValid (f * fileServer , path string ) bool {
113+ if dir , ok := f .fsys .(http.Dir ); ok {
114+ rootAbs , err := filepath .Abs (string (dir ))
115+ if err != nil {
116+ return false
117+ }
118+ rel := path
119+ if len (rel ) > 0 && rel [0 ] == '/' {
120+ rel = rel [1 :]
121+ }
122+ candidate := filepath .Join (rootAbs , filepath .FromSlash (rel ))
123+ fi , err := os .Lstat (candidate )
124+ if err != nil {
125+ return false
126+ }
127+ if fi .Mode ()& os .ModeSymlink != 0 {
128+ return false
129+ }
130+ }
131+ return true
126132}
127133
128134func withBase (w io.Writer , r io.Reader , base string ) error {
0 commit comments