diff --git a/assets b/assets index bf9c1ff7..0b722766 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit bf9c1ff7c196c6e3759bacdbf496d5dc2e2d2c35 +Subproject commit 0b722766f8808b0a0aaa3c9f3e4766972d746d22 diff --git a/pkg/wopi/wopi.go b/pkg/wopi/wopi.go index 9664afba..4b9f1cd9 100644 --- a/pkg/wopi/wopi.go +++ b/pkg/wopi/wopi.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/cloudreve/Cloudreve/v4/inventory/types" "net/url" "strings" "time" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" @@ -44,6 +45,7 @@ const ( LockTokenHeader = WopiHeaderPrefix + "Lock" ItemVersionHeader = WopiHeaderPrefix + "ItemVersion" SuggestedTargetHeader = WopiHeaderPrefix + "SuggestedTarget" + InvalidFileNameHeader = WopiHeaderPrefix + "InvalidFileNameError" MethodLock = "LOCK" MethodUnlock = "UNLOCK" diff --git a/service/explorer/viewer.go b/service/explorer/viewer.go index f8fd01b1..7454502c 100644 --- a/service/explorer/viewer.go +++ b/service/explorer/viewer.go @@ -218,6 +218,14 @@ func (service *WopiService) PutContent(c *gin.Context, isPutRelative bool) error return fmt.Errorf("failed to decode X-WOPI-SuggestedTarget header (UTF-7): %w", err) } + // X-WOPI-SuggestedTarget is a filename, not a path. Reject any value + // that could traverse out of the source file's directory. + if !isValidWopiSuggestedTarget(fileName) { + c.Status(http.StatusBadRequest) + c.Header(wopi.InvalidFileNameHeader, "Invalid target file name") + return nil + } + fileUriParsed, err := fs.NewUriFromString(fileUri) if err != nil { return fmt.Errorf("failed to parse file uri: %w", err) @@ -265,6 +273,19 @@ func (service *WopiService) PutContent(c *gin.Context, isPutRelative bool) error return nil } +// isValidWopiSuggestedTarget enforces that X-WOPI-SuggestedTarget is a bare +// filename (or extension prefixed with ".") and cannot traverse out of the +// source file's directory once joined onto it. +func isValidWopiSuggestedTarget(name string) bool { + if name == "" || name == "." || name == ".." { + return false + } + if strings.ContainsAny(name, "/\\") { + return false + } + return true +} + func (service *WopiService) GetFile(c *gin.Context) error { uri, m, _, viewerSession, dep, err := prepareFs(c) if err != nil { @@ -367,9 +388,9 @@ func (service *WopiService) FileInfo(c *gin.Context) (*WopiFileInfo, error) { type ( CreateViewerSessionService struct { - Uri string `json:"uri" form:"uri" binding:"required"` - Version string `json:"version" form:"version"` - ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"` + Uri string `json:"uri" form:"uri" binding:"required"` + Version string `json:"version" form:"version"` + ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"` PreferredAction types.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"` } CreateViewerSessionParamCtx struct{}