diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 639e630..a6d5440 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -124,6 +124,13 @@ func writeJSONError(w http.ResponseWriter, status int, message string) { _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) } 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, "\\", "/") for _, part := range strings.Split(p, "/") { 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) { a := account(r, s) - old := strings.Trim(r.FormValue("old"), "/") - neu := strings.Trim(r.FormValue("new"), "/") + rawOld, rawNew := r.FormValue("old"), 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() { writeJSONError(w, http.StatusForbidden, "has no privilege") return @@ -280,6 +292,10 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) { items = r.Form["path"] } for _, raw := range items { + if unsafePath(raw) { + writeJSONError(w, http.StatusBadRequest, "invalid parameter") + return + } p := strings.Trim(strings.ReplaceAll(raw, "\\", "/"), "/") if p == "" || !a.MaskFor(p).HasDelete() { 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) { - 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) if e != nil { 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) { a := account(r, s) - dir := strings.Trim(strings.ReplaceAll(r.FormValue("filepath"), "\\", "/"), "/") - filename := strings.Trim(r.FormValue("title"), "/") + rawDir, rawTitle, rawFilename := r.FormValue("filepath"), r.FormValue("title"), r.FormValue("filename") + 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 == "" { 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) { a := account(r, s) - dir := strings.Trim(r.FormValue("filepath"), "/") - name := strings.Trim(r.FormValue("filename"), "/") + rawDir, rawName := r.FormValue("filepath"), 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) if name == "" || !a.MaskFor(p).HasWrite() { writeJSONError(w, http.StatusForbidden, "has no privilege") @@ -424,7 +455,12 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) { return } 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() { writeJSONError(w, http.StatusForbidden, "has no privilege") return @@ -433,7 +469,7 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) { for _, hs := range r.MultipartForm.File { for _, h := range hs { 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) 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) { 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() { writeJSONError(w, http.StatusForbidden, "has no privilege") return @@ -480,7 +521,7 @@ func (s *Server) upload2(w http.ResponseWriter, r *http.Request) { for _, h := range hs { name := filepath.ToSlash(h.Filename) name = strings.TrimPrefix(name, "/") - if name == "" || strings.Contains(name, "../") || name == ".." { + if name == "" || unsafePath(name) { http.Error(w, "invalid filename", 400) 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) { - 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 == "" { http.Error(w, "file required", 400) 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) { a := account(r, s) - p := strings.TrimPrefix(r.URL.Path, "/chfs/downloaddir/") - p = strings.Trim(p, "/") + rawPath := strings.TrimPrefix(r.URL.Path, "/chfs/downloaddir/") + if unsafePath(rawPath) { + writeJSONError(w, http.StatusBadRequest, "invalid parameter") + return + } + p := strings.Trim(rawPath, "/") if !a.MaskFor(p).HasRead() { writeJSONError(w, http.StatusForbidden, "has no privilege") return @@ -765,8 +815,19 @@ func davStoragePath(name string) string { return strings.Trim(p, "/") } -func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) { +func davSafePath(name string) (string, error) { 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) if err != nil { 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) { - p := davStoragePath(name) + p, err := davSafePath(name) + if err != nil { + return nil, err + } fi, err := f.store.Stat(p) if err != nil { 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) { - root := davStoragePath(name) + root, err := davSafePath(name) + if err != nil { + return nil, err + } var out []webdav.FileInfo var walk func(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) { - p := davStoragePath(name) + p, err := davSafePath(name) + if err != nil { + return nil, false, err + } _, statErr := f.store.Stat(p) if statErr != nil && !os.IsNotExist(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 { - p := davStoragePath(name) + p, err := davSafePath(name) + if err != nil { + return err + } if p == "" { 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 { - p := davStoragePath(name) + p, err := davSafePath(name) + if err != nil { + return err + } if p == "" { 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) { - src := davStoragePath(name) - dst := davStoragePath(dest) + src, err := davSafePath(name) + if err != nil { + return false, err + } + dst, err := davSafePath(dest) + if err != nil { + return false, err + } if opts != nil && opts.NoOverwrite { if _, err := f.store.Stat(dst); err == nil { 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) { - src := davStoragePath(name) - dst := davStoragePath(dest) + src, err := davSafePath(name) + if err != nil { + return false, err + } + dst, err := davSafePath(dest) + if err != nil { + return false, err + } if opts == nil || opts.NoOverwrite { if _, err := f.store.Stat(dst); err == nil { return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))