fix(s3api): evaluate aws:SourceIp from the direct TCP peer, not forwarded headers (#11231)

* fix(s3api): use direct peer IP for aws:SourceIp in bucket policy engine

extractSourceIP in the bucket-policy engine trusted X-Forwarded-For and
X-Real-Ip whenever the TCP peer looked private (loopback/RFC1918/link-local),
with no configurable trusted-proxy allowlist. In containerized deployments
the gateway peer is almost always private, so any caller reaching it directly
or from a co-located workload could spoof aws:SourceIp and bypass
IpAddress/NotIpAddress bucket-policy restrictions.

Always return the direct peer address (r.RemoteAddr), matching AWS S3
semantics. Remove the now-unused isPrivateIP helper and header-trust branch.

Update TestExtractConditionValuesFromRequestSourceIPPrecedence to assert the
peer IP is used regardless of forwarding headers, and add regression tests
TestExtractSourceIP_IgnoresForwardedHeaders and
TestExtractSourceIP_EnforcesIPRestrictionPolicy.

* fix(s3api): use direct peer IP for aws:SourceIp in IAM role/session policies

The IAM middleware's extractSourceIP trusted X-Forwarded-For and X-Real-IP
whenever the TCP peer looked private (loopback/RFC1918/link-local), with no
configurable trusted-proxy allowlist. In containerized deployments the gateway
peer is almost always private, so any caller reaching it directly or from a
co-located workload could spoof aws:SourceIp and bypass IpAddress/NotIpAddress
conditions on role and session policies (IsPrincipalActionExplicitlyDenied).

Always return the direct peer address (r.RemoteAddr), matching AWS S3
semantics. Remove the now-unused isPrivateIP helper, privateNetworks table,
and its init().

Update TestRequestContextExtraction and TestIPBasedPolicyEnforcement to assert
the peer IP is enforced regardless of forwarding headers, and add regression
test TestUserInlinePolicySourceIpCondition_IgnoresForwardedHeaders.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-09-08 15:09:31 -07:00
1 parent 557fffa350
commit b88156fe6b
5 files changed
+99 -153

No files matched your search

@@ -123,6 +123,35 @@ func TestUserInlinePolicySourceIpCondition_Allows(t *testing.T) {
"PutObject from 127.0.0.1 must be allowed: the user inline policy's aws:SourceIp condition (127.0.0.0/8) matches")
}
// TestUserInlinePolicySourceIpCondition_IgnoresForwardedHeaders proves that a
// spoofed X-Forwarded-For cannot satisfy an aws:SourceIp condition: the policy
// requires 198.51.100.0/24, the peer is 127.0.0.1, and the header claims
// 198.51.100.5. The condition must be evaluated against the peer, not the
// header, so the request is denied.
func TestUserInlinePolicySourceIpCondition_IgnoresForwardedHeaders(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{}
seedInlineCondUser(t, api)
_, iamErr := api.PutUserPolicy(api.mockConfig, url.Values{
"UserName": {"alice"},
"PolicyName": {"OnlyFromTestNet"},
"PolicyDocument": {inlineCondPolicyDoc("198.51.100.0/24")},
})
require.Nil(t, iamErr, "PutUserPolicy must succeed")
require.NoError(t, api.PutS3ApiConfiguration(api.mockConfig))
require.NoError(t, api.iam.LoadS3ApiConfigurationFromCredentialManager())
ident := api.iam.lookupByIdentityName("alice")
require.NotNil(t, ident)
req := inlineCondRequest(t, http.MethodPut)
req.Header.Set("X-Forwarded-For", "198.51.100.5")
got := api.iam.VerifyActionPermission(req, ident, s3_constants.ACTION_WRITE, inlineCondTestBucket, "obj")
assert.Equal(t, s3err.ErrAccessDenied, got,
"PutObject from 127.0.0.1 must be denied despite X-Forwarded-For claiming 198.51.100.5: aws:SourceIp is the peer, not the header")
}
// TestGroupInlinePolicy_PutAndEnforce verifies that PutGroupPolicy is supported
// (no longer returns NotImplemented) and that an aws:SourceIp condition on the
// resulting inline policy is honored for group members.