Skip to content

Commit 9b67a5d

Browse files
committed
net/http: add protections against misuse of ServeFile
Martin Lenord pointed out that bad patterns have emerged in online examples of how to use ServeFile, where people pass r.URL.Path[1:] to ServeFile. This is unsafe. Document that it's unsafe, and add some protections. Fixes #14110 Change-Id: Ifeaa15534b2b3e46d3a8137be66748afa8fcd634 Reviewed-on: https://go-review.googlesource.com/18939 Reviewed-by: Andrew Gerrand <adg@golang.org> Reviewed-by: Russ Cox <rsc@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
1 parent 158f19b commit 9b67a5d

File tree

2 files changed

+60
-0
lines changed

2 files changed

+60
-0
lines changed

src/net/http/fs.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,15 +451,44 @@ func localRedirect(w ResponseWriter, r *Request, newPath string) {
451451
// ServeFile replies to the request with the contents of the named
452452
// file or directory.
453453
//
454+
// If the provided file or direcory name is a relative path, it is
455+
// interpreted relative to the current directory and may ascend to parent
456+
// directories. If the provided name is constructed from user input, it
457+
// should be sanitized before calling ServeFile. As a precaution, ServeFile
458+
// will reject requests where r.URL.Path contains a ".." path element.
459+
//
454460
// As a special case, ServeFile redirects any request where r.URL.Path
455461
// ends in "/index.html" to the same path, without the final
456462
// "index.html". To avoid such redirects either modify the path or
457463
// use ServeContent.
458464
func ServeFile(w ResponseWriter, r *Request, name string) {
465+
if containsDotDot(r.URL.Path) {
466+
// Too many programs use r.URL.Path to construct the argument to
467+
// serveFile. Reject the request under the assumption that happened
468+
// here and ".." may not be wanted.
469+
// Note that name might not contain "..", for example if code (still
470+
// incorrectly) used filepath.Join(myDir, r.URL.Path).
471+
Error(w, "invalid URL path", StatusBadRequest)
472+
return
473+
}
459474
dir, file := filepath.Split(name)
460475
serveFile(w, r, Dir(dir), file, false)
461476
}
462477

478+
func containsDotDot(v string) bool {
479+
if !strings.Contains(v, "..") {
480+
return false
481+
}
482+
for _, ent := range strings.FieldsFunc(v, isSlashRune) {
483+
if ent == ".." {
484+
return true
485+
}
486+
}
487+
return false
488+
}
489+
490+
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
491+
463492
type fileHandler struct {
464493
root FileSystem
465494
}

src/net/http/fs_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package http_test
66

77
import (
8+
"bufio"
89
"bytes"
910
"errors"
1011
"fmt"
@@ -177,6 +178,36 @@ Cases:
177178
}
178179
}
179180

181+
func TestServeFile_DotDot(t *testing.T) {
182+
tests := []struct {
183+
req string
184+
wantStatus int
185+
}{
186+
{"/testdata/file", 200},
187+
{"/../file", 400},
188+
{"/..", 400},
189+
{"/../", 400},
190+
{"/../foo", 400},
191+
{"/..\\foo", 400},
192+
{"/file/a", 200},
193+
{"/file/a..", 200},
194+
{"/file/a/..", 400},
195+
{"/file/a\\..", 400},
196+
}
197+
for _, tt := range tests {
198+
req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
199+
if err != nil {
200+
t.Errorf("bad request %q: %v", tt.req, err)
201+
continue
202+
}
203+
rec := httptest.NewRecorder()
204+
ServeFile(rec, req, "testdata/file")
205+
if rec.Code != tt.wantStatus {
206+
t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
207+
}
208+
}
209+
}
210+
180211
var fsRedirectTestData = []struct {
181212
original, redirect string
182213
}{

0 commit comments

Comments
 (0)