Skip to content

PostObjectV4: X-Amz-Security-Token form field name does not match lowercase policy condition (STS), S3 rejects with "Extra input fields" #3292

@paul7511

Description

@paul7511

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

  • Select this option if this issue appears to be a regression.

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

  1. 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.
  2. 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'
);
  1. POST the file with the produced getFormAttributes()['action'] URL and getFormInputs() fields (e.g. via curl -F ... -F 'file=@...').
  2. 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 has likely been latent since PR Properly set security token for S3 direct upload when using temporary credentials #1093 introduced STS support in PostObjectV4 in 2017.
  • 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 on 3.382.2 and current master)

Environment details (Version of PHP (php -v)? OS name and version, etc.)

PHP 8.4, Linux (Alpine in EKS), region ap-southeast-1, virtual-hosted-style addressing

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.needs-triageThis issue or PR still needs to be triaged.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions