-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Describe the bug
When generating presigned URLs with custom headers using the AWS SDK for PHP, requests fail with a 403 Forbidden "SignatureDoesNotMatch" error when used with Dell ECS. This happens because the SDK sorts headers alphabetically when creating the canonical request in createContext()
(in src/Signature/SignatureV4.php
, line 422), but does not maintain the same order in the X-Amz-SignedHeaders
parameter of the presigned URL in getPresignHeaders()
(in the same file, line 177).
Regression Issue
- Select this option if this issue appears to be a regression.
Expected Behavior
The AWS SDK for PHP should maintain a consistent order of headers throughout the signature process. Specifically, the order of headers in the X-Amz-SignedHeaders
parameter of the presigned URL should match the order used in the canonical request (alphabetically sorted).
Current Behavior
Currently, the SDK sorts headers alphabetically in createContext()
when creating the canonical request (in src/Signature/SignatureV4.php
, line 439):
// In src/Signature/SignatureV4.php, createContext() method, line 439
ksort($aggregate);
$canonHeaders = [];
foreach ($aggregate as $k => $v) {
if (count($v) > 0) {
sort($v);
}
$canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v));
}
$signedHeadersString = implode(';', array_keys($aggregate));
But it does not sort headers in getPresignHeaders()
when generating the X-Amz-SignedHeaders
parameter (in the same file, lines 177-192):
// In src/Signature/SignatureV4.php, getPresignHeaders() method, lines 177-192
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;
}
}
return $presignHeaders; // No sorting here!
}
This inconsistency causes the signature verification to fail because:
- The SDK calculates the signature using alphabetically sorted headers
- But it declares a different order in the presigned URL
- The S3 server uses the order declared in the URL to recreate the canonical request
- The resulting signature doesn't match the one provided
Reproduction Steps
<?php
require 'vendor/autoload.php';
use Aws\S3\S3Client;
// Configure the S3 client
$s3Client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => 'http://your-s3-endpoint',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'your-access-key',
'secret' => 'your-secret-key',
],
]);
// Generate a presigned URL with custom headers
$cmd = $s3Client->getCommand('GetObject', [
'Bucket' => 'your-bucket',
'Key' => 'your-object-key',
]);
// Add custom headers
$request = $s3Client->createPresignedRequest($cmd, '+1 hour', [
'headers' => [
'x-custom-header1' => 'value1',
'x-custom-header2' => 'value2',
]
]);
// Get the presigned URL
$presignedUrl = (string) $request->getUri();
echo $presignedUrl . "\n";
// When this URL is used with Dell ECS,
// it will fail with a 403 Forbidden "SignatureDoesNotMatch" error
Possible Solution
Sort the headers alphabetically in getPresignHeaders()
to maintain consistency with createContext()
:
// In src/Signature/SignatureV4.php, getPresignHeaders() method, lines 177-192
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); // Add this line to sort headers alphabetically
return $presignHeaders;
}
Additional Information/Context
This issue affects Dell ECS, but not AWS S3 itself. It seems AWS S3 is more forgiving about the order of headers, while Dell ECS strictly follows the order specified in the X-Amz-SignedHeaders
parameter.
We conducted a comprehensive analysis of all official AWS SDKs and found that this issue is unique to the PHP SDK. All other AWS SDKs (Go, Java, .NET, Python) correctly implement alphabetical sorting of headers at all stages of the signature process:
-
SDK AWS for Go:
- Uses
sort.Strings()
to sort headers alphabetically - Applies the same sorting consistently throughout the signature process
- Uses
-
SDK AWS for Java:
- Uses
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER)
to sort headers - Maintains the same sorted order in all signature-related methods
- Uses
-
SDK AWS for .NET:
- Uses
SortedDictionary<string, string>(StringComparer.Ordinal)
for headers - Ensures consistent ordering in all signature components
- Uses
-
SDK AWS for Python (boto3/botocore):
- Uses
sorted(set(headers_to_sign))
to sort headers alphabetically - Applies the same sorting in the
signed_headers
method
- Uses
This consistent implementation across all other SDKs strongly suggests that maintaining alphabetical ordering throughout the signature process is the expected behavior, and the PHP SDK's inconsistency is an oversight.
SDK version used
Latest (3.x)
Environment details (Version of PHP (php -v
)? OS name and version, etc.)
PHP 7.4+, tested on Linux