Browse Source
s3api: fix authentication bypass and potential SIGSEGV (Issue #7912) (#7954)
s3api: fix authentication bypass and potential SIGSEGV (Issue #7912) (#7954)
* s3api: fix authentication bypass and potential SIGSEGV * s3api: improve security tests with positive cases and nil identity guards * s3api: fix secondary authentication bypass in AuthSignatureOnly * s3api: refactor account loading and refine security tests based on review feedback * s3api: refine security tests with realistic signature failures * Update weed/s3api/auth_security_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>pull/7959/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 208 additions and 27 deletions
@ -0,0 +1,181 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"crypto/sha256" |
||||
|
"fmt" |
||||
|
"net/http" |
||||
|
"net/http/httptest" |
||||
|
"os" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/aws/aws-sdk-go-v2/aws" |
||||
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
func signRawHTTPRequest(ctx context.Context, req *http.Request, accessKey, secretKey, region string) error { |
||||
|
creds := aws.Credentials{ |
||||
|
AccessKeyID: accessKey, |
||||
|
SecretAccessKey: secretKey, |
||||
|
} |
||||
|
signer := v4.NewSigner() |
||||
|
payloadHash := fmt.Sprintf("%x", sha256.Sum256([]byte{})) |
||||
|
return signer.SignHTTP(ctx, creds, req, payloadHash, "s3", region, time.Now()) |
||||
|
} |
||||
|
|
||||
|
func TestReproIssue7912(t *testing.T) { |
||||
|
// Create a temporary s3.json
|
||||
|
configContent := `{ |
||||
|
"identities": [ |
||||
|
{ |
||||
|
"name": "xx", |
||||
|
"credentials": [ |
||||
|
{ |
||||
|
"accessKey": "xx_access_key", |
||||
|
"secretKey": "xx_secret_key" |
||||
|
} |
||||
|
], |
||||
|
"actions": ["Admin", "Read", "Write", "List", "Tagging"] |
||||
|
}, |
||||
|
{ |
||||
|
"name": "read_only_user", |
||||
|
"credentials": [ |
||||
|
{ |
||||
|
"accessKey": "readonly_access_key", |
||||
|
"secretKey": "readonly_secret_key" |
||||
|
} |
||||
|
], |
||||
|
"actions": ["Read", "List"] |
||||
|
} |
||||
|
] |
||||
|
}` |
||||
|
tmpFile, err := os.CreateTemp("", "s3-config-*.json") |
||||
|
assert.NoError(t, err) |
||||
|
defer os.Remove(tmpFile.Name()) |
||||
|
|
||||
|
_, err = tmpFile.Write([]byte(configContent)) |
||||
|
assert.NoError(t, err) |
||||
|
tmpFile.Close() |
||||
|
|
||||
|
// Initialize Identities Access Management
|
||||
|
option := &S3ApiServerOption{ |
||||
|
Config: tmpFile.Name(), |
||||
|
} |
||||
|
iam := NewIdentityAccessManagementWithStore(option, "memory") |
||||
|
|
||||
|
assert.True(t, iam.isEnabled(), "Auth should be enabled") |
||||
|
|
||||
|
// Test case 1: Unknown access key should be rejected
|
||||
|
t.Run("Unknown access key", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r.Host = "localhost:8333" |
||||
|
err := signRawHTTPRequest(context.Background(), r, "unknown_key", "any_secret", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrInvalidAccessKeyID, errCode, "Should be denied with unknown access key") |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
|
||||
|
t.Run("Positive test case: properly signed credentials", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r.Host = "localhost:8333" |
||||
|
err := signRawHTTPRequest(context.Background(), r, "readonly_access_key", "readonly_secret_key", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrNone, errCode) |
||||
|
require.NotNil(t, identity) |
||||
|
assert.Equal(t, "read_only_user", identity.Name) |
||||
|
}) |
||||
|
|
||||
|
t.Run("Nil identity tests for guards", func(t *testing.T) { |
||||
|
var nilIdentity *Identity |
||||
|
// Test isAdmin guard
|
||||
|
assert.False(t, nilIdentity.isAdmin()) |
||||
|
// Test canDo guard
|
||||
|
assert.False(t, nilIdentity.canDo(s3_constants.ACTION_LIST, "bucket", "object")) |
||||
|
}) |
||||
|
|
||||
|
t.Run("AuthSignatureOnly path", func(t *testing.T) { |
||||
|
// Valid request
|
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r.Host = "localhost:8333" |
||||
|
err := signRawHTTPRequest(context.Background(), r, "xx_access_key", "xx_secret_key", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
identity, errCode := iam.AuthSignatureOnly(r) |
||||
|
assert.Equal(t, s3err.ErrNone, errCode) |
||||
|
require.NotNil(t, identity) |
||||
|
assert.Equal(t, "xx", identity.Name) |
||||
|
|
||||
|
// Invalid request (wrong signature)
|
||||
|
r2 := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r2.Host = "localhost:8333" |
||||
|
err = signRawHTTPRequest(context.Background(), r2, "xx_access_key", "this_is_a_wrong_secret", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
_, errCode2 := iam.AuthSignatureOnly(r2) |
||||
|
assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode2) |
||||
|
|
||||
|
// Verify fix: Streaming unsigned payload should be denied without auth header in AuthSignatureOnly
|
||||
|
r3 := httptest.NewRequest(http.MethodPut, "http://localhost:8333/somebucket/someobject", nil) |
||||
|
r3.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER") |
||||
|
// No Authorization header
|
||||
|
_, errCode3 := iam.AuthSignatureOnly(r3) |
||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode3, "AuthSignatureOnly should be denied with unsigned streaming if no auth header") |
||||
|
}) |
||||
|
|
||||
|
t.Run("Wrong secret key", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r.Host = "localhost:8333" |
||||
|
err := signRawHTTPRequest(context.Background(), r, "readonly_access_key", "this_is_a_wrong_secret", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode, "Should NOT be allowed with wrong signature") |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
|
||||
|
t.Run("Anonymous request to protected bucket", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/somebucket/", nil) |
||||
|
// No Authorization header
|
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode, "Should be denied for anonymous") |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
|
||||
|
t.Run("Non-S3 request should be denied", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
// No headers at all
|
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode) |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
t.Run("Any other credentials", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodGet, "http://localhost:8333/", nil) |
||||
|
r.Host = "localhost:8333" |
||||
|
err := signRawHTTPRequest(context.Background(), r, "some_other_key", "some_secret", "us-east-1") |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_LIST) |
||||
|
assert.Equal(t, s3err.ErrInvalidAccessKeyID, errCode, "Should NOT be allowed with ANY other credentials") |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
t.Run("Streaming unsigned payload bypass attempt", func(t *testing.T) { |
||||
|
r := httptest.NewRequest(http.MethodPut, "http://localhost:8333/somebucket/someobject", nil) |
||||
|
r.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER") |
||||
|
// No Authorization header
|
||||
|
|
||||
|
identity, errCode := iam.authRequest(r, s3_constants.ACTION_WRITE) |
||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode, "Should be denied with unsigned streaming if no auth header") |
||||
|
assert.Nil(t, identity) |
||||
|
}) |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue