Describe the bug
Happy New Year to the AWS SDK team! 🎉
@stobrien89 - I'm following up on the original issue #3109 that you handled. I apologize for the delayed response - when PR #3201 was mentioned as potentially resolving the problem, I wanted to thoroughly test it before responding.
I've now tested the latest SDK version (3.369.9) and can confirm that the original issue persists.
The Problem:
The AWS SDK for PHP v3.369.9 contains an inconsistency in how headers are sorted during the signature v4 process for presigned URLs. Headers are sorted alphabetically in createContext() when calculating the signature, but are not sorted in getPresignHeaders() when building the X-Amz-SignedHeaders query parameter. This causes signature verification failures with S3-compatible services like Dell ECS.
Why PR #3201 doesn't fix this:
PR #3201 (merged in v3.369.5) fixed sorting of query parameters in getCanonicalizedQuery(), but our issue concerns headers in getPresignHeaders() - these are two different methods that were not both addressed.
Regression Issue
Expected Behavior
When generating presigned URLs with custom headers, the X-Amz-SignedHeaders query parameter should contain headers sorted alphabetically, matching the order used when calculating the signature in createContext().
Example:
X-Amz-SignedHeaders=a-custom-header;host;z-custom-header
(Headers sorted alphabetically)
This ensures consistency between:
- The order used to calculate the signature (in
createContext())
- The order declared in
X-Amz-SignedHeaders (used by S3 services for verification)
Current Behavior
Headers in X-Amz-SignedHeaders are returned in insertion order (not sorted), causing a mismatch with the alphabetically-sorted order used during signature calculation.
Example:
X-Amz-SignedHeaders=host;z-custom-header;a-custom-header
(Headers in insertion order, NOT alphabetically sorted)
Error from Dell ECS:
HTTP/1.1 403 Forbidden
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided.</Message>
</Error>
Dell ECS canonical request shows the inconsistency:
<CanonicalRequest>PUT
/testfile.txt
[...query parameters...]
a-my-test-header:valeur-de-test
host:test-bsz.s3.fr1.rec.lan:9020
host;a-my-test-header
UNSIGNED-PAYLOAD</CanonicalRequest>
Notice:
- Canonical headers section: alphabetically sorted (
a-my-test-header, then host)
- SignedHeaders line: NOT alphabetically sorted (
host;a-my-test-header)
Reproduction Steps
1. Create test file (test.php):
<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Middleware;
use Psr\Http\Message\RequestInterface;
$s3Client = new S3Client([
'version' => 'latest',
'endpoint' => 'http://your-s3-compatible-endpoint:9020',
'region' => 'us-east-1',
'credentials' => [
'key' => 'YOUR_ACCESS_KEY',
'secret' => 'YOUR_SECRET_KEY',
],
'signature_version' => 'v4',
]);
$cmd = $s3Client->getCommand('PutObject', [
'Bucket' => 'test-bucket',
'Key' => 'test-file.txt',
'ContentLength' => 100,
]);
// Add custom headers in non-alphabetical order
$cmd->getHandlerList()->appendBuild(
Middleware::mapRequest(function (RequestInterface $request) {
return $request
->withHeader('z-custom-header', 'value-z')
->withHeader('a-custom-header', 'value-a');
}),
'add-custom-headers'
);
$request = $s3Client->createPresignedRequest($cmd, '+20 minutes');
$signedUrl = (string) $request->getUri();
// Extract X-Amz-SignedHeaders
if (preg_match('/X-Amz-SignedHeaders=([^&]+)/', $signedUrl, $matches)) {
$signedHeaders = urldecode($matches[1]);
echo "Headers order: " . $signedHeaders . "\n";
// Expected: a-custom-header;host;z-custom-header (alphabetical)
// Actual: host;z-custom-header;a-custom-header (insertion order)
}
// Try to use the URL
$curlCmd = "curl -X PUT '$signedUrl' -H 'Host: your-endpoint' -H 'z-custom-header: value-z' -H 'a-custom-header: value-a' --data-binary '@test-file.txt'";
echo "Execute: $curlCmd\n";
2. Run the test:
3. Observe:
- Headers in
X-Amz-SignedHeaders are NOT alphabetically sorted
- Request fails with 403 SignatureDoesNotMatch on S3-compatible services that strictly validate signature v4
Possible Solution
Add sorting in the getPresignHeaders() method, similar to what was done in PR #3201 for query parameters.
Current code in src/Signature/SignatureV4.php (line ~285):
private function getPresignHeaders(array $headers)
{
$presignHeaders = [];
$blacklist = $this->getHeaderBlacklist();
foreach ($headers as $name => $value) {
$lName = strtolower($name);
if (!isset($blacklist[$lName])
&& $name !== self::AMZ_CONTENT_SHA256_HEADER
) {
$presignHeaders[] = $lName; // ❌ No sorting!
}
}
return $presignHeaders; // ❌ Returned unsorted!
}
Proposed fix:
private function getPresignHeaders(array $headers)
{
$presignHeaders = [];
$blacklist = $this->getHeaderBlacklist();
foreach ($headers as $name => $value) {
$lName = strtolower($name);
if (!isset($blacklist[$lName])
&& $name !== self::AMZ_CONTENT_SHA256_HEADER
) {
$presignHeaders[] = $lName;
}
}
sort($presignHeaders); // ✅ Sort alphabetically!
return $presignHeaders;
}
This single line addition ensures consistency with how headers are sorted in createContext() when calculating the signature.
Additional Information/Context
Related Issues and PRs
Why other AWS SDKs don't have this issue
We compared the PHP SDK with other official AWS SDKs - all consistently sort headers:
- AWS SDK for Go: Uses
sort.Strings() to sort headers
- AWS SDK for Java: Uses
Collections.sort() with case-insensitive comparison
- AWS SDK for .NET: Uses
SortedDictionary with ordinal comparison
- AWS SDK for Python (boto3): Uses
sorted() to sort headers
Impact
This issue affects:
- Any S3-compatible service that strictly validates signature v4 (like Dell ECS, MinIO, Ceph)
- Applications using presigned URLs with multiple custom headers
- Users migrating from other AWS SDKs to PHP
Workarounds
Three workarounds exist (documented in #3109):
- Custom implementation without the SDK
- Extending SignatureV4 class to override
getPresignHeaders()
- Using middleware to re-sort headers
However, a fix in the SDK would be preferable.
Test Results
Tested comprehensively on January 9, 2026 with:
- SDK v3.369.9 (latest version)
- Dell ECS v3.8.1.6
- Real production environment
- Result: ❌ 403 SignatureDoesNotMatch confirmed
References
SDK version used
3.369.9
Environment details (Version of PHP (php -v)? OS name and version, etc.)
PHP 8.4.0 (cli) - Linux (Docker container: php:8.4-cli)
Describe the bug
Happy New Year to the AWS SDK team! 🎉
@stobrien89 - I'm following up on the original issue #3109 that you handled. I apologize for the delayed response - when PR #3201 was mentioned as potentially resolving the problem, I wanted to thoroughly test it before responding.
I've now tested the latest SDK version (3.369.9) and can confirm that the original issue persists.
The Problem:
The AWS SDK for PHP v3.369.9 contains an inconsistency in how headers are sorted during the signature v4 process for presigned URLs. Headers are sorted alphabetically in
createContext()when calculating the signature, but are not sorted ingetPresignHeaders()when building theX-Amz-SignedHeadersquery parameter. This causes signature verification failures with S3-compatible services like Dell ECS.Why PR #3201 doesn't fix this:
PR #3201 (merged in v3.369.5) fixed sorting of query parameters in
getCanonicalizedQuery(), but our issue concerns headers ingetPresignHeaders()- these are two different methods that were not both addressed.Regression Issue
Expected Behavior
When generating presigned URLs with custom headers, the
X-Amz-SignedHeadersquery parameter should contain headers sorted alphabetically, matching the order used when calculating the signature increateContext().Example:
(Headers sorted alphabetically)
This ensures consistency between:
createContext())X-Amz-SignedHeaders(used by S3 services for verification)Current Behavior
Headers in
X-Amz-SignedHeadersare returned in insertion order (not sorted), causing a mismatch with the alphabetically-sorted order used during signature calculation.Example:
(Headers in insertion order, NOT alphabetically sorted)
Error from Dell ECS:
Dell ECS canonical request shows the inconsistency:
Notice:
a-my-test-header, thenhost)host;a-my-test-header)Reproduction Steps
1. Create test file (test.php):
2. Run the test:
3. Observe:
X-Amz-SignedHeadersare NOT alphabetically sortedPossible Solution
Add sorting in the
getPresignHeaders()method, similar to what was done in PR #3201 for query parameters.Current code in
src/Signature/SignatureV4.php(line ~285):Proposed fix:
This single line addition ensures consistency with how headers are sorted in
createContext()when calculating the signature.Additional Information/Context
Related Issues and PRs
Why other AWS SDKs don't have this issue
We compared the PHP SDK with other official AWS SDKs - all consistently sort headers:
sort.Strings()to sort headersCollections.sort()with case-insensitive comparisonSortedDictionarywith ordinal comparisonsorted()to sort headersImpact
This issue affects:
Workarounds
Three workarounds exist (documented in #3109):
getPresignHeaders()However, a fix in the SDK would be preferable.
Test Results
Tested comprehensively on January 9, 2026 with:
References
SDK version used
3.369.9
Environment details (Version of PHP (
php -v)? OS name and version, etc.)PHP 8.4.0 (cli) - Linux (Docker container: php:8.4-cli)