diff --git a/weed/s3api/auth_signature_v2.go b/weed/s3api/auth_signature_v2.go index 9f2167c43..2a61bfd85 100644 --- a/weed/s3api/auth_signature_v2.go +++ b/weed/s3api/auth_signature_v2.go @@ -147,7 +147,7 @@ func (iam *IdentityAccessManagement) doesSignV2Match(r *http.Request) (*Identity return nil, s3err.ErrAccessDenied } - expectedAuth := signatureV2(cred, r.Method, r.URL.Path, r.URL.Query().Encode(), r.Header) + expectedAuth := signatureV2(cred, r.Method, r.URL.EscapedPath(), r.URL.Query().Encode(), r.Header) // Extract signatures from both auth headers v2Signature := "" @@ -224,7 +224,7 @@ func (iam *IdentityAccessManagement) doesPresignV2SignatureMatch(r *http.Request return nil, s3err.ErrAccessDenied } - expectedSignature := preSignatureV2(cred, r.Method, r.URL.Path, r.URL.Query().Encode(), r.Header, expires) + expectedSignature := preSignatureV2(cred, r.Method, r.URL.EscapedPath(), r.URL.Query().Encode(), r.Header, expires) if !compareSignatureV2(signature, expectedSignature) { return nil, s3err.ErrSignatureDoesNotMatch } diff --git a/weed/s3api/auth_signature_v2_test.go b/weed/s3api/auth_signature_v2_test.go index 01b7156aa..f036b4b29 100644 --- a/weed/s3api/auth_signature_v2_test.go +++ b/weed/s3api/auth_signature_v2_test.go @@ -2,7 +2,9 @@ package s3api import ( "net/http" + "strconv" "testing" + "time" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" @@ -287,3 +289,79 @@ func TestDoesSignV2Match(t *testing.T) { }) } } + +func TestDoesSignV2MatchUnicodePath(t *testing.T) { + iam := setupTestIAMForV2Auth() + cred := &Credential{ + AccessKey: "AKIAIOSFODNN7EXAMPLE", + SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + } + + tests := []struct { + name string + path string + }{ + { + name: "CJK conversation key from ai-dial-core", + path: "/dialcore/core/Users/test-user/conversations/qwen3.5-35b-a3b__%E5%A4%9A%E7%9D%87%E3%80%82", + }, + { + name: "Japanese filename from issue 8598", + path: "/voyage-dev1/public/1773200112088-998898991/%E3%81%9F%E3%81%82%E3%81%84%E3%81%BEd%C2%B7.jpg", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Date", "Mon, 09 Sep 2011 23:36:00 GMT") + + // Clients (jclouds, AWS SDK) sign percent-encoded paths, not decoded Path. + authHeader := signatureV2(cred, req.Method, tt.path, req.URL.Query().Encode(), req.Header) + req.Header.Set("Authorization", authHeader) + + identity, errCode := iam.doesSignV2Match(req) + if errCode != s3err.ErrNone { + t.Fatalf("doesSignV2Match() error = %v, want %v; path=%q escaped=%q", + errCode, s3err.ErrNone, req.URL.Path, req.URL.EscapedPath()) + } + if identity == nil { + t.Fatal("Expected non-nil identity") + } + }) + } +} + +func TestDoesPresignV2MatchUnicodePath(t *testing.T) { + iam := setupTestIAMForV2Auth() + cred := &Credential{ + AccessKey: "AKIAIOSFODNN7EXAMPLE", + SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + } + + path := "/dialcore/core/Users/test-user/conversations/qwen3.5-35b-a3b__%E5%A4%9A%E7%9D%87%E3%80%82" + expires := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10) + + req, err := http.NewRequest(http.MethodGet, "http://example.com"+path, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + query := req.URL.Query() + query.Set("AWSAccessKeyId", cred.AccessKey) + query.Set("Expires", expires) + query.Set("Signature", preSignatureV2(cred, req.Method, path, query.Encode(), req.Header, expires)) + req.URL.RawQuery = query.Encode() + + identity, errCode := iam.doesPresignV2SignatureMatch(req) + if errCode != s3err.ErrNone { + t.Fatalf("doesPresignV2SignatureMatch() error = %v, want %v; path=%q escaped=%q", + errCode, s3err.ErrNone, req.URL.Path, req.URL.EscapedPath()) + } + if identity == nil { + t.Fatal("Expected non-nil identity") + } +}