Skip to content

Inconsistent header ordering in presigned URLs with custom headers causes SignatureDoesNotMatch errors #3109

@DisasteR

Description

@DisasteR

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:

  1. The SDK calculates the signature using alphabetically sorted headers
  2. But it declares a different order in the presigned URL
  3. The S3 server uses the order declared in the URL to recreate the canonical request
  4. 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:

  1. SDK AWS for Go:

    • Uses sort.Strings() to sort headers alphabetically
    • Applies the same sorting consistently throughout the signature process
  2. 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
  3. SDK AWS for .NET:

    • Uses SortedDictionary<string, string>(StringComparer.Ordinal) for headers
    • Ensures consistent ordering in all signature components
  4. 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

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

Metadata

Metadata

Labels

bugThis issue is a bug.investigatingThis issue is being investigated and/or work is in progress to resolve the issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions