mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 22:23:48 +03:00
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:
parent
2ebfda1ae9
commit
33c70f418f
6 changed files with 62 additions and 3 deletions
|
@ -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 = "."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
1
modules/caddyhttp/fileserver/testdata/%D9%85%D9%84%D9%81.txt
vendored
Normal file
1
modules/caddyhttp/fileserver/testdata/%D9%85%D9%84%D9%81.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
%D9%85%D9%84%D9%81.txt
|
1
modules/caddyhttp/fileserver/testdata/ملف.txt
vendored
Normal file
1
modules/caddyhttp/fileserver/testdata/ملف.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ملف.txt
|
Loading…
Reference in a new issue