fileserver: properly handle escaped/non-ascii paths (#4332)

* fileserver: properly handle escaped/non-ascii paths

* fileserver: tests: accommodate Windows hate of colons in files names
This commit is contained in:
Mohammed Al Sahaf 2021-09-16 23:40:31 +03:00 committed by GitHub
parent 2ebfda1ae9
commit 33c70f418f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 62 additions and 3 deletions

View file

@ -20,6 +20,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -227,6 +228,7 @@ func StatusCodeMatches(actual, configured int) bool {
// never be outside of root. The resulting path can be used // never be outside of root. The resulting path can be used
// with the local file system. // with the local file system.
func SanitizedPathJoin(root, reqPath string) string { func SanitizedPathJoin(root, reqPath string) string {
reqPath, _ = url.PathUnescape(reqPath)
if root == "" { if root == "" {
root = "." root = "."
} }

View file

@ -42,15 +42,16 @@ func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root
isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath) isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath)
u := url.URL{Path: url.PathEscape(name)}
// add the slash after the escape of path to avoid escaping the slash as well
if isDir { if isDir {
name += "/" u.Path += "/"
dirCount++ dirCount++
} else { } else {
fileCount++ fileCount++
} }
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
fileInfos = append(fileInfos, fileInfo{ fileInfos = append(fileInfos, fileInfo{
IsDir: isDir, IsDir: isDir,
IsSymlink: isSymlink(f), IsSymlink: isSymlink(f),

View file

@ -17,12 +17,31 @@ package fileserver
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"os"
"runtime"
"testing" "testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func TestFileMatcher(t *testing.T) { func TestFileMatcher(t *testing.T) {
// Windows doesn't like colons in files names
isWindows := runtime.GOOS == "windows"
if !isWindows {
filename := "with:in-name.txt"
f, err := os.Create("./testdata/" + filename)
if err != nil {
t.Fail()
return
}
t.Cleanup(func() {
os.Remove("./testdata/" + filename)
})
f.WriteString(filename)
f.Close()
}
for i, tc := range []struct { for i, tc := range []struct {
path string path string
expectedPath string expectedPath string
@ -63,6 +82,30 @@ func TestFileMatcher(t *testing.T) {
path: "/missingfile.php", path: "/missingfile.php",
matched: false, matched: false,
}, },
{
path: "ملف.txt", // the path file name is not escaped
expectedPath: "ملف.txt",
expectedType: "file",
matched: true,
},
{
path: url.PathEscape("ملف.txt"), // singly-escaped path
expectedPath: "ملف.txt",
expectedType: "file",
matched: true,
},
{
path: url.PathEscape(url.PathEscape("ملف.txt")), // doubly-escaped path
expectedPath: "%D9%85%D9%84%D9%81.txt",
expectedType: "file",
matched: true,
},
{
path: "./with:in-name.txt", // browsers send the request with the path as such
expectedPath: "with:in-name.txt",
expectedType: "file",
matched: !isWindows,
},
} { } {
m := &MatchFile{ m := &MatchFile{
Root: "./testdata", Root: "./testdata",

View file

@ -19,6 +19,7 @@ import (
weakrand "math/rand" weakrand "math/rand"
"mime" "mime"
"net/http" "net/http"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -165,6 +166,16 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
root := repl.ReplaceAll(fsrv.Root, ".") root := repl.ReplaceAll(fsrv.Root, ".")
// PathUnescape returns an error if the escapes aren't well-formed,
// meaning the count % matches the RFC. Return early if the escape is
// improper.
if _, err := url.PathUnescape(r.URL.Path); err != nil {
fsrv.logger.Debug("improper path escape",
zap.String("site_root", root),
zap.String("request_path", r.URL.Path),
zap.Error(err))
return err
}
filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path) filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path)
fsrv.logger.Debug("sanitized path join", fsrv.logger.Debug("sanitized path join",

View file

@ -0,0 +1 @@
%D9%85%D9%84%D9%81.txt

View file

@ -0,0 +1 @@
ملف.txt