security: harden path inputs across API and WebDAV
This commit is contained in:
+112
-24
@@ -124,6 +124,13 @@ func writeJSONError(w http.ResponseWriter, status int, message string) {
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
func unsafePath(p string) bool {
|
func unsafePath(p string) bool {
|
||||||
|
// Reject traversal and NUL before any permission/storage resolution.
|
||||||
|
// URL.Query/FormValue has already performed the single HTTP decode, so
|
||||||
|
// double-encoded traversal remains a literal filename rather than being
|
||||||
|
// decoded again by this layer.
|
||||||
|
if strings.IndexByte(p, 0) >= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
p = strings.ReplaceAll(p, "\\", "/")
|
p = strings.ReplaceAll(p, "\\", "/")
|
||||||
for _, part := range strings.Split(p, "/") {
|
for _, part := range strings.Split(p, "/") {
|
||||||
if part == ".." {
|
if part == ".." {
|
||||||
@@ -258,8 +265,13 @@ func (s *Server) newdir(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) rename(w http.ResponseWriter, r *http.Request) {
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
old := strings.Trim(r.FormValue("old"), "/")
|
rawOld, rawNew := r.FormValue("old"), r.FormValue("new")
|
||||||
neu := strings.Trim(r.FormValue("new"), "/")
|
if unsafePath(rawOld) || unsafePath(rawNew) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
old := strings.Trim(rawOld, "/")
|
||||||
|
neu := strings.Trim(rawNew, "/")
|
||||||
if old == "" || neu == "" || !a.MaskFor(old).HasWrite() || !a.MaskFor(path.Dir(neu)).HasWrite() {
|
if old == "" || neu == "" || !a.MaskFor(old).HasWrite() || !a.MaskFor(path.Dir(neu)).HasWrite() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
return
|
return
|
||||||
@@ -280,6 +292,10 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
|
|||||||
items = r.Form["path"]
|
items = r.Form["path"]
|
||||||
}
|
}
|
||||||
for _, raw := range items {
|
for _, raw := range items {
|
||||||
|
if unsafePath(raw) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
p := strings.Trim(strings.ReplaceAll(raw, "\\", "/"), "/")
|
p := strings.Trim(strings.ReplaceAll(raw, "\\", "/"), "/")
|
||||||
if p == "" || !a.MaskFor(p).HasDelete() {
|
if p == "" || !a.MaskFor(p).HasDelete() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
@@ -319,7 +335,12 @@ func (s *Server) removePath(p string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
|
||||||
p := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/")
|
raw := r.URL.Query().Get("file")
|
||||||
|
if unsafePath(raw) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p := strings.Trim(strings.ReplaceAll(raw, "\\", "/"), "/")
|
||||||
_, e := s.Store.Stat(p)
|
_, e := s.Store.Stat(p)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@@ -372,8 +393,13 @@ func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
dir := strings.Trim(strings.ReplaceAll(r.FormValue("filepath"), "\\", "/"), "/")
|
rawDir, rawTitle, rawFilename := r.FormValue("filepath"), r.FormValue("title"), r.FormValue("filename")
|
||||||
filename := strings.Trim(r.FormValue("title"), "/")
|
if unsafePath(rawDir) || unsafePath(rawTitle) || unsafePath(rawFilename) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := strings.Trim(strings.ReplaceAll(rawDir, "\\", "/"), "/")
|
||||||
|
filename := strings.Trim(rawTitle, "/")
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
filename = strings.Trim(r.FormValue("filename"), "/")
|
filename = strings.Trim(r.FormValue("filename"), "/")
|
||||||
}
|
}
|
||||||
@@ -398,8 +424,13 @@ func (s *Server) newtext(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
func (s *Server) updatetext(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) updatetext(w http.ResponseWriter, r *http.Request) {
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
dir := strings.Trim(r.FormValue("filepath"), "/")
|
rawDir, rawName := r.FormValue("filepath"), r.FormValue("filename")
|
||||||
name := strings.Trim(r.FormValue("filename"), "/")
|
if unsafePath(rawDir) || unsafePath(rawName) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := strings.Trim(strings.ReplaceAll(rawDir, "\\", "/"), "/")
|
||||||
|
name := strings.Trim(strings.ReplaceAll(rawName, "\\", "/"), "/")
|
||||||
p := path.Join(dir, name)
|
p := path.Join(dir, name)
|
||||||
if name == "" || !a.MaskFor(p).HasWrite() {
|
if name == "" || !a.MaskFor(p).HasWrite() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
@@ -424,7 +455,12 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
dir := strings.Trim(r.FormValue("folder"), "/")
|
rawDir := r.FormValue("folder")
|
||||||
|
if unsafePath(rawDir) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := strings.Trim(strings.ReplaceAll(rawDir, "\\", "/"), "/")
|
||||||
if !a.MaskFor(dir).HasWrite() {
|
if !a.MaskFor(dir).HasWrite() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
return
|
return
|
||||||
@@ -433,7 +469,7 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, hs := range r.MultipartForm.File {
|
for _, hs := range r.MultipartForm.File {
|
||||||
for _, h := range hs {
|
for _, h := range hs {
|
||||||
name := filepath.ToSlash(h.Filename)
|
name := filepath.ToSlash(h.Filename)
|
||||||
if name == "" || strings.HasPrefix(name, "/") || strings.Contains(name, "../") || name == ".." {
|
if name == "" || strings.HasPrefix(name, "/") || unsafePath(name) {
|
||||||
http.Error(w, "invalid filename", 400)
|
http.Error(w, "invalid filename", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -466,7 +502,12 @@ func savePart(st storage.Storage, target string, h *multipart.FileHeader) error
|
|||||||
|
|
||||||
func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
dir := strings.Trim(r.URL.Query().Get("folder"), "/")
|
rawDir := r.URL.Query().Get("folder")
|
||||||
|
if unsafePath(rawDir) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := strings.Trim(strings.ReplaceAll(rawDir, "\\", "/"), "/")
|
||||||
if !a.MaskFor(dir).HasWrite() {
|
if !a.MaskFor(dir).HasWrite() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
return
|
return
|
||||||
@@ -480,7 +521,7 @@ func (s *Server) upload2(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, h := range hs {
|
for _, h := range hs {
|
||||||
name := filepath.ToSlash(h.Filename)
|
name := filepath.ToSlash(h.Filename)
|
||||||
name = strings.TrimPrefix(name, "/")
|
name = strings.TrimPrefix(name, "/")
|
||||||
if name == "" || strings.Contains(name, "../") || name == ".." {
|
if name == "" || unsafePath(name) {
|
||||||
http.Error(w, "invalid filename", 400)
|
http.Error(w, "invalid filename", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -603,7 +644,12 @@ func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
|
||||||
file := strings.Trim(strings.ReplaceAll(r.URL.Query().Get("file"), "\\", "/"), "/")
|
rawFile := r.URL.Query().Get("file")
|
||||||
|
if unsafePath(rawFile) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file := strings.Trim(strings.ReplaceAll(rawFile, "\\", "/"), "/")
|
||||||
if file == "" {
|
if file == "" {
|
||||||
http.Error(w, "file required", 400)
|
http.Error(w, "file required", 400)
|
||||||
return
|
return
|
||||||
@@ -673,8 +719,12 @@ func (s *Server) serveFile(w http.ResponseWriter, r *http.Request, p string, att
|
|||||||
|
|
||||||
func (s *Server) downloadDir(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) downloadDir(w http.ResponseWriter, r *http.Request) {
|
||||||
a := account(r, s)
|
a := account(r, s)
|
||||||
p := strings.TrimPrefix(r.URL.Path, "/chfs/downloaddir/")
|
rawPath := strings.TrimPrefix(r.URL.Path, "/chfs/downloaddir/")
|
||||||
p = strings.Trim(p, "/")
|
if unsafePath(rawPath) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p := strings.Trim(rawPath, "/")
|
||||||
if !a.MaskFor(p).HasRead() {
|
if !a.MaskFor(p).HasRead() {
|
||||||
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
writeJSONError(w, http.StatusForbidden, "has no privilege")
|
||||||
return
|
return
|
||||||
@@ -765,8 +815,19 @@ func davStoragePath(name string) string {
|
|||||||
return strings.Trim(p, "/")
|
return strings.Trim(p, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
|
func davSafePath(name string) (string, error) {
|
||||||
p := davStoragePath(name)
|
p := davStoragePath(name)
|
||||||
|
if unsafePath(p) {
|
||||||
|
return "", webdav.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid parameter"))
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
|
||||||
|
p, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
fi, err := f.store.Stat(p)
|
fi, err := f.store.Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -775,7 +836,10 @@ func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
func (f *webdavStorageFS) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||||
p := davStoragePath(name)
|
p, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
fi, err := f.store.Stat(p)
|
fi, err := f.store.Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -791,7 +855,10 @@ func (f *webdavStorageFS) Stat(ctx context.Context, name string) (*webdav.FileIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
|
||||||
root := davStoragePath(name)
|
root, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var out []webdav.FileInfo
|
var out []webdav.FileInfo
|
||||||
var walk func(string) error
|
var walk func(string) error
|
||||||
walk = func(dir string) error {
|
walk = func(dir string) error {
|
||||||
@@ -817,7 +884,10 @@ func (f *webdavStorageFS) ReadDir(ctx context.Context, name string, recursive bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadCloser, opts *webdav.CreateOptions) (*webdav.FileInfo, bool, error) {
|
||||||
p := davStoragePath(name)
|
p, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
_, statErr := f.store.Stat(p)
|
_, statErr := f.store.Stat(p)
|
||||||
if statErr != nil && !os.IsNotExist(statErr) {
|
if statErr != nil && !os.IsNotExist(statErr) {
|
||||||
return nil, false, statErr
|
return nil, false, statErr
|
||||||
@@ -840,7 +910,10 @@ func (f *webdavStorageFS) Create(ctx context.Context, name string, body io.ReadC
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webdav.RemoveAllOptions) error {
|
||||||
p := davStoragePath(name)
|
p, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return fmt.Errorf("cannot remove storage root")
|
return fmt.Errorf("cannot remove storage root")
|
||||||
}
|
}
|
||||||
@@ -868,7 +941,10 @@ func (f *webdavStorageFS) RemoveAll(ctx context.Context, name string, opts *webd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
|
func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
|
||||||
p := davStoragePath(name)
|
p, err := davSafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return fmt.Errorf("cannot create storage root")
|
return fmt.Errorf("cannot create storage root")
|
||||||
}
|
}
|
||||||
@@ -881,8 +957,14 @@ func (f *webdavStorageFS) Mkdir(ctx context.Context, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *webdav.CopyOptions) (bool, error) {
|
||||||
src := davStoragePath(name)
|
src, err := davSafePath(name)
|
||||||
dst := davStoragePath(dest)
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
dst, err := davSafePath(dest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
if opts != nil && opts.NoOverwrite {
|
if opts != nil && opts.NoOverwrite {
|
||||||
if _, err := f.store.Stat(dst); err == nil {
|
if _, err := f.store.Stat(dst); err == nil {
|
||||||
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
|
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
|
||||||
@@ -940,8 +1022,14 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) {
|
||||||
src := davStoragePath(name)
|
src, err := davSafePath(name)
|
||||||
dst := davStoragePath(dest)
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
dst, err := davSafePath(dest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
if opts == nil || opts.NoOverwrite {
|
if opts == nil || opts.NoOverwrite {
|
||||||
if _, err := f.store.Stat(dst); err == nil {
|
if _, err := f.store.Stat(dst); err == nil {
|
||||||
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
|
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
|
||||||
|
|||||||
Reference in New Issue
Block a user