mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-18 21:26:56 +00:00
* make TUS max upload size and session expiry configurable * default TUS session expiry to 24h
115 lines
4.7 KiB
Go
115 lines
4.7 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// TestFilerServer_tusHandler_CrossPrefixSessionHijack reproduces
|
|
// GHSA-99q7-x53r-6j4g: a prefix-restricted token acting on another tenant's TUS
|
|
// session (HEAD/PATCH/DELETE) must be scoped against the session's stored
|
|
// TargetPath, not authorized on signature and method alone. The victim's session
|
|
// must survive a denied mutation.
|
|
func TestFilerServer_tusHandler_CrossPrefixSessionHijack(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
prefix string
|
|
expectStatus int
|
|
expectExists bool
|
|
}{
|
|
{"cross-prefix HEAD denied", http.MethodHead, "/buckets/allowed", http.StatusUnauthorized, true},
|
|
{"matching-prefix HEAD allowed", http.MethodHead, "/buckets/secret", http.StatusOK, true},
|
|
{"cross-prefix PATCH denied", http.MethodPatch, "/buckets/allowed", http.StatusUnauthorized, true},
|
|
{"matching-prefix PATCH allowed", http.MethodPatch, "/buckets/secret", http.StatusNoContent, true},
|
|
{"cross-prefix DELETE denied", http.MethodDelete, "/buckets/allowed", http.StatusUnauthorized, true},
|
|
{"matching-prefix DELETE allowed", http.MethodDelete, "/buckets/secret", http.StatusNoContent, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fs, store := newTusTestServer(t, map[string]string{tusTestUploadID: "/buckets/secret/victim.bin"})
|
|
|
|
signingKey := tusTestWriteKey
|
|
if tt.method == http.MethodHead {
|
|
signingKey = tusTestReadKey
|
|
}
|
|
token := signFilerToken(t, signingKey, []string{tt.prefix}, nil)
|
|
req := httptest.NewRequest(tt.method, "/.tus/.uploads/"+tusTestUploadID, http.NoBody)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Tus-Resumable", TusVersion)
|
|
if tt.method == http.MethodPatch {
|
|
req.Header.Set("Content-Type", "application/offset+octet-stream")
|
|
req.Header.Set("Upload-Offset", "0")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
|
|
fs.tusHandler(rec, req)
|
|
|
|
if rec.Code != tt.expectStatus {
|
|
t.Fatalf("%s status = %d, want %d; body=%q", tt.method, rec.Code, tt.expectStatus, rec.Body.String())
|
|
}
|
|
_, err := store.FindEntry(context.Background(), util.FullPath(fs.tusSessionInfoPath(tusTestUploadID)))
|
|
if tt.expectExists && err != nil {
|
|
t.Fatalf("session removed after %s: %v", tt.method, err)
|
|
}
|
|
if !tt.expectExists && err == nil {
|
|
t.Fatalf("session still present after authorized %s", tt.method)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFilerServer_tusHandler_RejectsAliasesAndInvalidMetadata covers the routing
|
|
// and metadata guards: a non-canonical or aliased upload id is rejected before
|
|
// any lookup, and a session whose stored id, target or size is unusable resolves
|
|
// to "not found" rather than being authorized or acted upon.
|
|
func TestFilerServer_tusHandler_RejectsAliasesAndInvalidMetadata(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
routeID string
|
|
stored TusSession
|
|
}{
|
|
{"trailing path alias", tusTestUploadID + "/extra", TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: 1}},
|
|
{"non-canonical route id", "not-a-uuid", TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: 1}},
|
|
{"stored id mismatch", tusTestUploadID, TusSession{ID: "00000000-0000-0000-0000-000000000000", TargetPath: "/buckets/secret/victim.bin", Size: 1}},
|
|
{"empty stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "", Size: 1}},
|
|
{"root stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "/", Size: 1}},
|
|
{"relative stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "buckets/secret/x.bin", Size: 1}},
|
|
{"oversize stored size", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: TusDefaultMaxSize + 1}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fs, store := newTusTestServer(t, nil)
|
|
data, err := json.Marshal(&tt.stored)
|
|
if err != nil {
|
|
t.Fatalf("marshal session: %v", err)
|
|
}
|
|
if err := store.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: util.FullPath(fs.tusSessionInfoPath(tusTestUploadID)),
|
|
Content: data,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodHead, "/.tus/.uploads/"+tt.routeID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+signFilerToken(t, tusTestReadKey, nil, nil))
|
|
req.Header.Set("Tus-Resumable", TusVersion)
|
|
rec := httptest.NewRecorder()
|
|
|
|
fs.tusHandler(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("HEAD %s = %d, want %d; body=%q", tt.routeID, rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|