Skip to content

Commit 2daf214

Browse files
fix: skip unsignable headers (#1207)
Co-authored-by: Mattias Kindborg <[email protected]>
1 parent 225ec89 commit 2daf214

File tree

11 files changed

+256
-13
lines changed

11 files changed

+256
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) and is followi
66

77
## Unreleased
88

9+
### :syringe: Fixed
10+
11+
- [#1198](https://github.com/FantasticFiasco/aws-signature-version-4/issues/1198) Requests with certain HTTP headers are rejected by API Gateway. (contribution by [@cfbao](https://github.com/cfbao))
12+
913
## [4.0.6] - 2024-09-18
1014

1115
### :syringe: Fixed

src/Private/CanonicalRequest.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ public static class CanonicalRequest
3030
/// </summary>
3131
public static string HeaderValueSeparator { get; set; } = ", ";
3232

33+
// Including most headers from
34+
// https://github.com/smithy-lang/smithy-typescript/blob/430021abf44f8a4d6c24de2dfa25709bf91a92c8/packages/signature-v4/src/constants.ts#L19-L35
35+
private static readonly HashSet<string> UnsignableHeaders =
36+
[
37+
"connection",
38+
"expect",
39+
"keep-alive",
40+
"proxy-authenticate",
41+
"proxy-authorization",
42+
"proxy-connection",
43+
"range",
44+
"te",
45+
"trailer",
46+
"transfer-encoding",
47+
"upgrade",
48+
HeaderKeys.XAmznTraceIdHeader
49+
];
50+
3351
/// <returns>
3452
/// The first value is the canonical request, the second value is the signed headers.
3553
/// </returns>
@@ -91,8 +109,10 @@ public static (string, string) Build(
91109
builder.Append($"{string.Join("&", parameters)}\n");
92110

93111
// Add the canonical headers, followed by a newline character. The canonical headers
94-
// consist of a list of all the HTTP headers that you are including with the signed
95-
// request.
112+
// consist of a list of HTTP headers that you are including with the signed request.
113+
//
114+
// Some headers are unsignable, i.e. signatures including them are always deemed invalid.
115+
// They are excluded from the canonical headers (but will be kept in the HTTP request).
96116
//
97117
// To create the canonical headers list, convert all header names to lowercase and
98118
// remove leading spaces and trailing spaces. Convert sequential spaces in the header
@@ -108,7 +128,7 @@ public static (string, string) Build(
108128
// PLEASE NOTE: Microsoft has chosen to separate the header values with ", ", not ","
109129
// as defined by the Canonical Request algorithm.
110130
// - Append a new line ('\n').
111-
var sortedHeaders = SortHeaders(request.Headers, defaultHeaders);
131+
var sortedHeaders = PruneAndSortHeaders(request.Headers, defaultHeaders);
112132

113133
foreach (var header in sortedHeaders)
114134
{
@@ -187,7 +207,7 @@ public static SortedList<string, List<string>> SortQueryParameters(string query)
187207
return sortedQueryParameters;
188208
}
189209

190-
public static SortedDictionary<string, List<string>> SortHeaders(
210+
public static SortedDictionary<string, List<string>> PruneAndSortHeaders(
191211
HttpRequestHeaders headers,
192212
IEnumerable<KeyValuePair<string, IEnumerable<string>>> defaultHeaders)
193213
{
@@ -202,6 +222,11 @@ void AddHeader(KeyValuePair<string, IEnumerable<string>> header)
202222
{
203223
var headerName = FormatHeaderName(header.Key);
204224

225+
if (UnsignableHeaders.Contains(headerName))
226+
{
227+
return;
228+
}
229+
205230
// Create header if it doesn't already exist
206231
if (!sortedHeaders.TryGetValue(headerName, out var headerValues))
207232
{

test/Integration/ApiGateway/AwsSignatureHandlerShould.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using AwsSignatureVersion4.Integration.ApiGateway.Requests;
88
using AwsSignatureVersion4.Private;
99
using AwsSignatureVersion4.TestSuite;
10+
using AwsSignatureVersion4.Unit.Private;
1011
using Shouldly;
1112
using Xunit;
1213

@@ -316,6 +317,30 @@ public async Task SucceedGivenUnorderedQuery(
316317
receivedRequest.Body.ShouldBeNull();
317318
}
318319

320+
[Theory]
321+
[MemberData(nameof(TestCases))]
322+
public async Task SucceedGivenUnsignableHeaders(
323+
IamAuthenticationType iamAuthenticationType,
324+
HttpMethod method)
325+
{
326+
// Arrange
327+
using var httpClient = HttpClientFactory(iamAuthenticationType).CreateClient("integration");
328+
329+
var request = new HttpRequestMessage(method, Context.ApiGatewayUrl);
330+
CanonicalRequestShould.AddUnsignableHeaders(request);
331+
332+
// Act
333+
var response = await httpClient.SendAsync(request);
334+
335+
// Assert
336+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
337+
338+
var receivedRequest = await response.Content.ReadReceivedRequestAsync();
339+
receivedRequest.Method.ShouldBe(method.ToString());
340+
receivedRequest.Path.ShouldBe("/");
341+
receivedRequest.Body.ShouldBeNull();
342+
}
343+
319344
[Theory]
320345
[MemberData(nameof(TestCases))]
321346
public async Task SucceedGivenHttpCompletionOption(

test/Integration/ApiGateway/SendAsyncShould.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using AwsSignatureVersion4.Integration.ApiGateway.Requests;
88
using AwsSignatureVersion4.Private;
99
using AwsSignatureVersion4.TestSuite;
10+
using AwsSignatureVersion4.Unit.Private;
1011
using Shouldly;
1112
using Xunit;
1213

@@ -662,6 +663,41 @@ public async Task SucceedGivenUnorderedQuery(
662663
receivedRequest.Body.ShouldBeNull();
663664
}
664665

666+
[Theory]
667+
[InlineData(IamAuthenticationType.User, "GET")]
668+
[InlineData(IamAuthenticationType.User, "POST")]
669+
[InlineData(IamAuthenticationType.User, "PUT")]
670+
[InlineData(IamAuthenticationType.User, "PATCH")]
671+
[InlineData(IamAuthenticationType.User, "DELETE")]
672+
[InlineData(IamAuthenticationType.Role, "GET")]
673+
[InlineData(IamAuthenticationType.Role, "POST")]
674+
[InlineData(IamAuthenticationType.Role, "PUT")]
675+
[InlineData(IamAuthenticationType.Role, "PATCH")]
676+
[InlineData(IamAuthenticationType.Role, "DELETE")]
677+
public async Task SucceedGivenUnsignableHeaders(
678+
IamAuthenticationType iamAuthenticationType,
679+
string method)
680+
{
681+
// Arrange
682+
var request = new HttpRequestMessage(new HttpMethod(method), Context.ApiGatewayUrl);
683+
CanonicalRequestShould.AddUnsignableHeaders(request);
684+
685+
// Act
686+
var response = await HttpClient.SendAsync(
687+
request,
688+
Context.RegionName,
689+
Context.ServiceName,
690+
ResolveMutableCredentials(iamAuthenticationType));
691+
692+
// Assert
693+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
694+
695+
var receivedRequest = await response.Content.ReadReceivedRequestAsync();
696+
receivedRequest.Method.ShouldBe(method);
697+
receivedRequest.Path.ShouldBe("/");
698+
receivedRequest.Body.ShouldBeNull();
699+
}
700+
665701
internal static HttpRequestMessage BuildRequest(
666702
TestSuiteContext testSuiteContext,
667703
IntegrationTestContext integrationTestContext,
@@ -676,7 +712,7 @@ internal static HttpRequestMessage BuildRequest(
676712
.ToUri();
677713

678714
// The "Host" header is now invalid since we redirected the request to the AWS API
679-
// Gateway. Lets remove the header and have the signature implementation re-add it
715+
// Gateway. Let's remove the header and have the signature implementation re-add it
680716
// correctly.
681717
request.Headers.Remove("Host");
682718

test/Integration/ApiGateway/SendShould.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using AwsSignatureVersion4.Integration.ApiGateway.Requests;
88
using AwsSignatureVersion4.Private;
99
using AwsSignatureVersion4.TestSuite;
10+
using AwsSignatureVersion4.Unit.Private;
1011
using Shouldly;
1112
using Xunit;
1213

@@ -661,5 +662,40 @@ public async Task SucceedGivenUnorderedQuery(
661662
receivedRequest.QueryStringParameters["Param1"].ShouldBe(new[] { "Value2", "Value1" });
662663
receivedRequest.Body.ShouldBeNull();
663664
}
665+
666+
[Theory]
667+
[InlineData(IamAuthenticationType.User, "GET")]
668+
[InlineData(IamAuthenticationType.User, "POST")]
669+
[InlineData(IamAuthenticationType.User, "PUT")]
670+
[InlineData(IamAuthenticationType.User, "PATCH")]
671+
[InlineData(IamAuthenticationType.User, "DELETE")]
672+
[InlineData(IamAuthenticationType.Role, "GET")]
673+
[InlineData(IamAuthenticationType.Role, "POST")]
674+
[InlineData(IamAuthenticationType.Role, "PUT")]
675+
[InlineData(IamAuthenticationType.Role, "PATCH")]
676+
[InlineData(IamAuthenticationType.Role, "DELETE")]
677+
public async Task SucceedGivenUSucceedGivenUnsignableHeadersnorderedQuery(
678+
IamAuthenticationType iamAuthenticationType,
679+
string method)
680+
{
681+
// Arrange
682+
var request = new HttpRequestMessage(new HttpMethod(method), Context.ApiGatewayUrl);
683+
CanonicalRequestShould.AddUnsignableHeaders(request);
684+
685+
// Act
686+
var response = HttpClient.Send(
687+
request,
688+
Context.RegionName,
689+
Context.ServiceName,
690+
ResolveMutableCredentials(iamAuthenticationType));
691+
692+
// Assert
693+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
694+
695+
var receivedRequest = await response.Content.ReadReceivedRequestAsync();
696+
receivedRequest.Method.ShouldBe(method);
697+
receivedRequest.Path.ShouldBe("/");
698+
receivedRequest.Body.ShouldBeNull();
699+
}
664700
}
665701
}

test/Integration/S3/AwsSignatureHandlerShould.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using AwsSignatureVersion4.Integration.ApiGateway.Authentication;
55
using AwsSignatureVersion4.Private;
66
using AwsSignatureVersion4.TestSuite;
7+
using AwsSignatureVersion4.Unit.Private;
78
using Shouldly;
89
using Xunit;
910

@@ -136,6 +137,26 @@ public async Task SucceedGivenHttpCompletionOption(IamAuthenticationType iamAuth
136137
response.StatusCode.ShouldBe(HttpStatusCode.OK);
137138
}
138139

140+
[Theory]
141+
[InlineData(IamAuthenticationType.User)]
142+
[InlineData(IamAuthenticationType.Role)]
143+
public async Task SucceedGivenUnsignableHeaders(IamAuthenticationType iamAuthenticationType)
144+
{
145+
// Arrange
146+
var bucketObject = await Bucket.PutObjectAsync(BucketObjectKey.WithoutPrefix);
147+
148+
using var httpClient = HttpClientFactory(iamAuthenticationType).CreateClient("integration");
149+
var requestUri = $"{Context.S3BucketUrl}/{bucketObject.Key}";
150+
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
151+
CanonicalRequestShould.AddUnsignableHeaders(request);
152+
153+
// Act
154+
var response = await httpClient.SendAsync(request);
155+
156+
// Assert
157+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
158+
}
159+
139160
private async Task UploadRequiredObjectAsync(params string[] scenarioName)
140161
{
141162
if (scenarioName[0] == "get-unreserved" || scenarioName[0] == "get-vanilla-query-unreserved")

test/Integration/S3/SendAsyncShould.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using AwsSignatureVersion4.Integration.ApiGateway.Authentication;
55
using AwsSignatureVersion4.Private;
66
using AwsSignatureVersion4.TestSuite;
7+
using AwsSignatureVersion4.Unit.Private;
78
using Shouldly;
89
using Xunit;
910

@@ -124,6 +125,28 @@ public async Task PassTestSuiteGivenAssumedRole(params string[] scenarioName)
124125
response.StatusCode.ShouldBe(HttpStatusCode.OK);
125126
}
126127

128+
[Theory]
129+
[InlineData(IamAuthenticationType.User)]
130+
[InlineData(IamAuthenticationType.Role)]
131+
public async Task SucceedGivenUnsignableHeaders(IamAuthenticationType iamAuthenticationType)
132+
{
133+
// Arrange
134+
var bucketObject = await Bucket.PutObjectAsync(BucketObjectKey.WithoutPrefix);
135+
var requestUri = $"{Context.S3BucketUrl}/{bucketObject.Key}";
136+
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
137+
CanonicalRequestShould.AddUnsignableHeaders(request);
138+
139+
// Act
140+
var response = await HttpClient.SendAsync(
141+
request,
142+
Context.RegionName,
143+
Context.ServiceName,
144+
ResolveMutableCredentials(iamAuthenticationType));
145+
146+
// Assert
147+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
148+
}
149+
127150
[Theory]
128151
[InlineData(IamAuthenticationType.User)]
129152
[InlineData(IamAuthenticationType.Role)]

test/Integration/S3/SendShould.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using AwsSignatureVersion4.Integration.ApiGateway.Authentication;
55
using AwsSignatureVersion4.TestSuite;
6+
using AwsSignatureVersion4.Unit.Private;
67
using Shouldly;
78
using Xunit;
89

@@ -123,6 +124,28 @@ public async Task PassTestSuiteGivenAssumedRole(params string[] scenarioName)
123124
response.StatusCode.ShouldBe(HttpStatusCode.OK);
124125
}
125126

127+
[Theory]
128+
[InlineData(IamAuthenticationType.User)]
129+
[InlineData(IamAuthenticationType.Role)]
130+
public async Task SucceedGivenUnsignableHeaders(IamAuthenticationType iamAuthenticationType)
131+
{
132+
// Arrange
133+
var bucketObject = await Bucket.PutObjectAsync(BucketObjectKey.WithoutPrefix);
134+
var requestUri = $"{Context.S3BucketUrl}/{bucketObject.Key}";
135+
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
136+
CanonicalRequestShould.AddUnsignableHeaders(request);
137+
138+
// Act
139+
var response = HttpClient.Send(
140+
request,
141+
Context.RegionName,
142+
Context.ServiceName,
143+
ResolveMutableCredentials(iamAuthenticationType));
144+
145+
// Assert
146+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
147+
}
148+
126149
[Theory]
127150
[InlineData(IamAuthenticationType.User)]
128151
[InlineData(IamAuthenticationType.Role)]

test/Unit/Private/CanonicalRequestGivenDotnetShould.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void RespectDefaultHeader()
3333
defaultHeaders.Add("some-header-name", "some-header-value");
3434

3535
// Act
36-
var actual = CanonicalRequest.SortHeaders(headers, defaultHeaders);
36+
var actual = CanonicalRequest.PruneAndSortHeaders(headers, defaultHeaders);
3737

3838
// Assert
3939
actual["some-header-name"].ShouldBe(new[] { "some-header-value" });
@@ -50,7 +50,7 @@ public void IgnoreDuplicateDefaultHeader()
5050
defaultHeaders.Add("some-header-name", "some-ignored-header-value");
5151

5252
// Act
53-
var actual = CanonicalRequest.SortHeaders(headers, defaultHeaders);
53+
var actual = CanonicalRequest.PruneAndSortHeaders(headers, defaultHeaders);
5454

5555
// Assert
5656
actual["some-header-name"].ShouldBe(new[] { "some-header-value" });

test/Unit/Private/CanonicalRequestGivenMonoShould.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void RespectDefaultHeader()
3333
defaultHeaders.Add("some-header-name", "some-header-value");
3434

3535
// Act
36-
var actual = CanonicalRequest.SortHeaders(headers, defaultHeaders);
36+
var actual = CanonicalRequest.PruneAndSortHeaders(headers, defaultHeaders);
3737

3838
// Assert
3939
actual["some-header-name"].ShouldBe(new[] { "some-header-value" });
@@ -50,7 +50,7 @@ public void RespectDuplicateDefaultHeader()
5050
defaultHeaders.Add("some-header-name", "some-other-header-value");
5151

5252
// Act
53-
var actual = CanonicalRequest.SortHeaders(headers, defaultHeaders);
53+
var actual = CanonicalRequest.PruneAndSortHeaders(headers, defaultHeaders);
5454

5555
// Assert
5656
actual["some-header-name"].ShouldBe(new[] { "some-header-value", "some-other-header-value" });

0 commit comments

Comments
 (0)