Describe the bug
Aws\S3\PostObjectV4 produces inconsistent casing for X-Amz-Security-Token between the policy condition and the actual form field. When the S3 client uses temporary credentials (STS — any source: AssumeRole profile, EKS IRSA, EKS Pod Identity, EC2 instance role), the generated presigned POST is rejected by S3.
The other four SigV4 fields in the same function (X-Amz-Date, X-Amz-Credential, X-Amz-Algorithm, X-Amz-Signature) are consistently camel-cased on both sides. Only x-amz-security-token (the STS one) is lowercased on the policy side but camel-cased on the form side.
This appears to be related in pattern to #3274 / #2882 (lowercase blacklist vs camel-cased header in SignatureV4), but in a different file and code path.
Regression Issue
(This has likely been latent since PR #1093 introduced STS support in PostObjectV4 in 2017.)
Expected Behavior
PostObjectV4 should emit matching casing for X-Amz-Security-Token in both the policy condition and the form field, so that S3 accepts the POST when using temporary credentials — consistent with how the other four SigV4 fields are handled in the same function.
Current Behavior
src/S3/PostObjectV4.php lines 58–60:
if ($securityToken = $credentials->getSecurityToken()) {
$options [] = ['x-amz-security-token' => $securityToken]; // policy condition: lowercase
$formInputs['X-Amz-Security-Token'] = $securityToken; // form field: CamelCase
}
S3 enforces case-sensitive matching between form field names and policy condition keys (at least for SigV4 + virtual-hosted bucket in regions such as ap-southeast-1). Because the form field X-Amz-Security-Token does not have a matching X-Amz-Security-Token entry in the policy conditions, S3 rejects the upload as having unexpected fields.
A real captured policy (base64-decoded) shows the mismatch clearly:
{
"expiration": "2026-05-28T03:18:32Z",
"conditions": [
["eq", "$key", "pending/share/.../test.html"],
["eq", "$Content-Type", "text/html"],
["content-length-range", 1, 10485760],
{"x-amz-security-token": "IQoJ..."},
{"X-Amz-Date": "20260528T031332Z"},
{"X-Amz-Credential": "ASIA.../20260528/ap-southeast-1/s3/aws4_request"},
{"X-Amz-Algorithm": "AWS4-HMAC-SHA256"}
]
}
…while the form fields emitted by getFormInputs() are:
key, Content-Type, X-Amz-Security-Token, X-Amz-Credential, X-Amz-Algorithm, X-Amz-Date, Policy, X-Amz-Signature
Note X-Amz-Security-Token (CamelCase) is the only field whose name does not exactly match any condition key in the policy.
Reproduction Steps
-
Use any source of STS temporary credentials (AWS_PROFILE with role_arn, AssumeRoleWithWebIdentity, EKS IRSA / Pod Identity, EC2 instance role). The access key will be prefixed ASIA* and Credentials::getSecurityToken() returns non-empty.
-
Construct an Aws\S3\PostObjectV4 against a virtual-hosted-style bucket in a SigV4 region (reproduced in ap-southeast-1):
$client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ap-southeast-1',
// credentials resolved from the default chain — STS-backed in this case
]);
$post = new \Aws\S3\PostObjectV4(
$client,
'my-bucket',
['key' => 'pending/test.html', 'Content-Type' => 'text/html'],
[
['eq', '$key', 'pending/test.html'],
['eq', '$Content-Type', 'text/html'],
['content-length-range', 1, 10_485_760],
],
'+5 minutes'
);
-
POST the file with the produced getFormAttributes()['action'] URL and getFormInputs() fields (e.g. via curl -F ... -F 'file=@...').
-
S3 rejects with an Extra input fields error referencing X-Amz-Security-Token.
Possible Solution
Change line 59 so the policy condition uses the same casing as the form field. The form field name (X-Amz-Security-Token) is the AWS-documented casing for HTTP transit; matching it on the policy side is consistent with how the other four SigV4 fields are handled in the same function:
if ($securityToken = $credentials->getSecurityToken()) {
$options [] = ['X-Amz-Security-Token' => $securityToken]; // CamelCase, matches form
$formInputs['X-Amz-Security-Token'] = $securityToken;
}
This brings X-Amz-Security-Token into line with the existing camel-cased treatment of X-Amz-Date / X-Amz-Credential / X-Amz-Algorithm in getPolicyAndSignature().
Additional Information/Context
- This bug is invisible when using static long-lived IAM user credentials (
AKIA*) because getSecurityToken() returns null and the buggy branch is skipped.
- It surfaces in any environment that uses STS — which, since EKS Pod Identity / IRSA have effectively become the recommended way to give AWS permissions to workloads, is increasingly common.
- Currently impacted projects appear to be working around it by switching to presigned PUT URLs or by reverting to static IAM keys.
SDK version used
3.380.3 (also confirmed present on 3.382.2 and current master)
Environment details
PHP 8.4, Linux (Alpine in EKS), region ap-southeast-1, virtual-hosted-style addressing.
Describe the bug
Aws\S3\PostObjectV4produces inconsistent casing forX-Amz-Security-Tokenbetween the policy condition and the actual form field. When the S3 client uses temporary credentials (STS — any source:AssumeRoleprofile, EKS IRSA, EKS Pod Identity, EC2 instance role), the generated presigned POST is rejected by S3.The other four SigV4 fields in the same function (
X-Amz-Date,X-Amz-Credential,X-Amz-Algorithm,X-Amz-Signature) are consistently camel-cased on both sides. Onlyx-amz-security-token(the STS one) is lowercased on the policy side but camel-cased on the form side.This appears to be related in pattern to #3274 / #2882 (lowercase blacklist vs camel-cased header in
SignatureV4), but in a different file and code path.Regression Issue
(This has likely been latent since PR #1093 introduced STS support in
PostObjectV4in 2017.)Expected Behavior
PostObjectV4should emit matching casing forX-Amz-Security-Tokenin both the policy condition and the form field, so that S3 accepts the POST when using temporary credentials — consistent with how the other four SigV4 fields are handled in the same function.Current Behavior
src/S3/PostObjectV4.phplines 58–60:S3 enforces case-sensitive matching between form field names and policy condition keys (at least for SigV4 + virtual-hosted bucket in regions such as
ap-southeast-1). Because the form fieldX-Amz-Security-Tokendoes not have a matchingX-Amz-Security-Tokenentry in the policyconditions, S3 rejects the upload as having unexpected fields.A real captured policy (base64-decoded) shows the mismatch clearly:
{ "expiration": "2026-05-28T03:18:32Z", "conditions": [ ["eq", "$key", "pending/share/.../test.html"], ["eq", "$Content-Type", "text/html"], ["content-length-range", 1, 10485760], {"x-amz-security-token": "IQoJ..."}, {"X-Amz-Date": "20260528T031332Z"}, {"X-Amz-Credential": "ASIA.../20260528/ap-southeast-1/s3/aws4_request"}, {"X-Amz-Algorithm": "AWS4-HMAC-SHA256"} ] }…while the form fields emitted by
getFormInputs()are:Note
X-Amz-Security-Token(CamelCase) is the only field whose name does not exactly match any condition key in the policy.Reproduction Steps
Use any source of STS temporary credentials (
AWS_PROFILEwithrole_arn,AssumeRoleWithWebIdentity, EKS IRSA / Pod Identity, EC2 instance role). The access key will be prefixedASIA*andCredentials::getSecurityToken()returns non-empty.Construct an
Aws\S3\PostObjectV4against a virtual-hosted-style bucket in a SigV4 region (reproduced inap-southeast-1):POST the file with the produced
getFormAttributes()['action']URL andgetFormInputs()fields (e.g. viacurl -F ... -F 'file=@...').S3 rejects with an
Extra input fieldserror referencingX-Amz-Security-Token.Possible Solution
Change line 59 so the policy condition uses the same casing as the form field. The form field name (
X-Amz-Security-Token) is the AWS-documented casing for HTTP transit; matching it on the policy side is consistent with how the other four SigV4 fields are handled in the same function:This brings
X-Amz-Security-Tokeninto line with the existing camel-cased treatment ofX-Amz-Date/X-Amz-Credential/X-Amz-AlgorithmingetPolicyAndSignature().Additional Information/Context
AKIA*) becausegetSecurityToken()returns null and the buggy branch is skipped.SDK version used
3.380.3(also confirmed present on3.382.2and currentmaster)Environment details
PHP 8.4, Linux (Alpine in EKS), region
ap-southeast-1, virtual-hosted-style addressing.