Skip to content

Commit d1a23e2

Browse files
authored
Normalize URLs during signing (except for S3). (#3534)
During signing when generating the canonical request, request paths are supposed to be normalized to remove . and .. path components. We were not doing this before, making it possible to construct requests that fail signature validation.
1 parent 2c0ce2d commit d1a23e2

File tree

13 files changed

+302
-105
lines changed

13 files changed

+302
-105
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Fixed an issue that could result in signature mismatch exceptions when requests included . or .."
6+
}

core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public final class AwsSignerExecutionAttribute extends SdkExecutionAttribute {
5858
*/
5959
public static final ExecutionAttribute<Boolean> SIGNER_DOUBLE_URL_ENCODE = new ExecutionAttribute<>("DoubleUrlEncode");
6060

61+
/**
62+
* The key to specify whether to normalize the resource path during signing.
63+
*/
64+
public static final ExecutionAttribute<Boolean> SIGNER_NORMALIZE_PATH =
65+
new ExecutionAttribute<>("NormalizePath");
66+
6167
/**
6268
* The key to specify the expiration time when pre-signing aws requests.
6369
*/

core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import java.time.Instant;
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
27+
import java.util.Collections;
2728
import java.util.Comparator;
2829
import java.util.List;
30+
import java.util.SortedMap;
31+
import java.util.TreeMap;
2932
import software.amazon.awssdk.annotations.SdkInternalApi;
3033
import software.amazon.awssdk.auth.credentials.AwsCredentials;
3134
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
@@ -41,6 +44,7 @@
4144
import software.amazon.awssdk.core.internal.util.HttpChecksumUtils;
4245
import software.amazon.awssdk.core.signer.Presigner;
4346
import software.amazon.awssdk.http.SdkHttpFullRequest;
47+
import software.amazon.awssdk.http.SdkHttpRequest;
4448
import software.amazon.awssdk.utils.BinaryUtils;
4549
import software.amazon.awssdk.utils.Logger;
4650
import software.amazon.awssdk.utils.Pair;
@@ -81,6 +85,7 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
8185
ContentChecksum contentChecksum) {
8286

8387
SdkHttpFullRequest.Builder mutableRequest = request.toBuilder();
88+
8489
AwsCredentials sanitizedCredentials = sanitizeCredentials(signingParams.awsCredentials());
8590
if (sanitizedCredentials instanceof AwsSessionCredentials) {
8691
addSessionCredentials(mutableRequest, (AwsSessionCredentials) sanitizedCredentials);
@@ -97,9 +102,11 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
97102
putChecksumHeader(signingParams.checksumParams(), contentChecksum.contentFlexibleChecksum(),
98103
mutableRequest, contentChecksum.contentHash());
99104

100-
CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest,
105+
CanonicalRequest canonicalRequest = createCanonicalRequest(request,
106+
mutableRequest,
101107
contentChecksum.contentHash(),
102-
signingParams.doubleUrlEncode());
108+
signingParams.doubleUrlEncode(),
109+
signingParams.normalizePath());
103110

104111
String canonicalRequestString = canonicalRequest.string();
105112
String stringToSign = createStringToSign(canonicalRequestString, requestParams);
@@ -138,8 +145,11 @@ protected SdkHttpFullRequest.Builder doPresign(SdkHttpFullRequest request,
138145
// Add the important parameters for v4 signing
139146
String contentSha256 = calculateContentHashPresign(mutableRequest, signingParams);
140147

141-
CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest, contentSha256,
142-
signingParams.doubleUrlEncode());
148+
CanonicalRequest canonicalRequest = createCanonicalRequest(request,
149+
mutableRequest,
150+
contentSha256,
151+
signingParams.doubleUrlEncode(),
152+
signingParams.normalizePath());
143153

144154
addPreSignInformationToRequest(mutableRequest, canonicalRequest, sanitizedCredentials,
145155
requestParams, expirationInSeconds);
@@ -237,10 +247,12 @@ protected final byte[] deriveSigningKey(AwsCredentials credentials, Instant sign
237247
* .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to
238248
* generate the canonical request.
239249
*/
240-
private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder request,
250+
private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest request,
251+
SdkHttpFullRequest.Builder requestBuilder,
241252
String contentSha256,
242-
boolean doubleUrlEncode) {
243-
return new CanonicalRequest(request, contentSha256, doubleUrlEncode);
253+
boolean doubleUrlEncode,
254+
boolean normalizePath) {
255+
return new CanonicalRequest(request, requestBuilder, contentSha256, doubleUrlEncode, normalizePath);
244256
}
245257

246258
/**
@@ -251,6 +263,8 @@ private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder reque
251263
private String createStringToSign(String canonicalRequest,
252264
Aws4SignerRequestParams requestParams) {
253265

266+
LOG.debug(() -> "AWS4 Canonical Request: " + canonicalRequest);
267+
254268
String requestHash = BinaryUtils.toHex(hash(canonicalRequest));
255269

256270
String stringToSign = requestParams.getSigningAlgorithm() +
@@ -330,7 +344,7 @@ private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRe
330344
* @param ch the character to be tested
331345
* @return true if the character is white space, false otherwise.
332346
*/
333-
private boolean isWhiteSpace(final char ch) {
347+
private static boolean isWhiteSpace(char ch) {
334348
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f';
335349
}
336350

@@ -402,8 +416,15 @@ protected <B extends Aws4SignerParams.Builder> B extractSignerParams(B paramsBui
402416
.signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
403417
.timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET));
404418

405-
if (executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE) != null) {
406-
paramsBuilder.doubleUrlEncode(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE));
419+
Boolean doubleUrlEncode = executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE);
420+
if (doubleUrlEncode != null) {
421+
paramsBuilder.doubleUrlEncode(doubleUrlEncode);
422+
}
423+
424+
Boolean normalizePath =
425+
executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH);
426+
if (normalizePath != null) {
427+
paramsBuilder.normalizePath(normalizePath);
407428
}
408429
ChecksumSpecs checksumSpecs = executionAttributes.getAttribute(RESOLVED_CHECKSUM_SPECS);
409430
if (checksumSpecs != null && checksumSpecs.algorithm() != null) {
@@ -453,30 +474,41 @@ private SdkChecksum createSdkChecksumFromParams(T signingParams, SdkHttpFullRequ
453474
return null;
454475
}
455476

456-
private class CanonicalRequest {
457-
private final SdkHttpFullRequest.Builder request;
477+
static final class CanonicalRequest {
478+
private final SdkHttpFullRequest request;
479+
private final SdkHttpFullRequest.Builder requestBuilder;
458480
private final String contentSha256;
459481
private final boolean doubleUrlEncode;
482+
private final boolean normalizePath;
460483

461484
private String canonicalRequestString;
462485
private StringBuilder signedHeaderStringBuilder;
463486
private List<Pair<String, List<String>>> canonicalHeaders;
464487
private String signedHeaderString;
465488

466-
private CanonicalRequest(SdkHttpFullRequest.Builder request, String contentSha256, boolean doubleUrlEncode) {
489+
CanonicalRequest(SdkHttpFullRequest request,
490+
SdkHttpFullRequest.Builder requestBuilder,
491+
String contentSha256,
492+
boolean doubleUrlEncode,
493+
boolean normalizePath) {
467494
this.request = request;
495+
this.requestBuilder = requestBuilder;
468496
this.contentSha256 = contentSha256;
469497
this.doubleUrlEncode = doubleUrlEncode;
498+
this.normalizePath = normalizePath;
470499
}
471500

472501
public String string() {
473502
if (canonicalRequestString == null) {
474503
StringBuilder canonicalRequest = new StringBuilder(512);
475-
canonicalRequest.append(request.method().toString())
504+
canonicalRequest.append(requestBuilder.method().toString())
476505
.append(SignerConstant.LINE_SEPARATOR);
477-
addCanonicalizedResourcePath(canonicalRequest, request.encodedPath(), doubleUrlEncode);
506+
addCanonicalizedResourcePath(canonicalRequest,
507+
request,
508+
doubleUrlEncode,
509+
normalizePath);
478510
canonicalRequest.append(SignerConstant.LINE_SEPARATOR);
479-
addCanonicalizedQueryString(canonicalRequest, request);
511+
addCanonicalizedQueryString(canonicalRequest, requestBuilder);
480512
canonicalRequest.append(SignerConstant.LINE_SEPARATOR);
481513
addCanonicalizedHeaderString(canonicalRequest, canonicalHeaders());
482514
canonicalRequest.append(SignerConstant.LINE_SEPARATOR)
@@ -488,6 +520,82 @@ public String string() {
488520
return canonicalRequestString;
489521
}
490522

523+
private void addCanonicalizedResourcePath(StringBuilder result,
524+
SdkHttpRequest request,
525+
boolean urlEncode,
526+
boolean normalizePath) {
527+
String path = normalizePath ? request.getUri().normalize().getRawPath()
528+
: request.encodedPath();
529+
530+
if (StringUtils.isEmpty(path)) {
531+
result.append("/");
532+
return;
533+
}
534+
535+
if (urlEncode) {
536+
path = SdkHttpUtils.urlEncodeIgnoreSlashes(path);
537+
}
538+
539+
if (!path.startsWith("/")) {
540+
result.append("/");
541+
}
542+
result.append(path);
543+
544+
// Normalization can leave a trailing slash at the end of the resource path,
545+
// even if the input path doesn't end with one. Example input: /foo/bar/.
546+
// Remove the trailing slash if the input path doesn't end with one.
547+
boolean trimTrailingSlash = normalizePath &&
548+
path.length() > 1 &&
549+
!request.encodedPath().endsWith("/") &&
550+
result.charAt(result.length() - 1) == '/';
551+
if (trimTrailingSlash) {
552+
result.setLength(result.length() - 1);
553+
}
554+
}
555+
556+
/**
557+
* Examines the specified query string parameters and returns a
558+
* canonicalized form.
559+
* <p>
560+
* The canonicalized query string is formed by first sorting all the query
561+
* string parameters, then URI encoding both the key and value and then
562+
* joining them, in order, separating key value pairs with an '&amp;'.
563+
*
564+
* @return A canonicalized form for the specified query string parameters.
565+
*/
566+
private void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) {
567+
568+
SortedMap<String, List<String>> sorted = new TreeMap<>();
569+
570+
/**
571+
* Signing protocol expects the param values also to be sorted after url
572+
* encoding in addition to sorted parameter names.
573+
*/
574+
httpRequest.forEachRawQueryParameter((key, values) -> {
575+
if (StringUtils.isEmpty(key)) {
576+
// Do not sign empty keys.
577+
return;
578+
}
579+
580+
String encodedParamName = SdkHttpUtils.urlEncode(key);
581+
582+
List<String> encodedValues = new ArrayList<>(values.size());
583+
for (String value : values) {
584+
String encodedValue = SdkHttpUtils.urlEncode(value);
585+
586+
// Null values should be treated as empty for the purposes of signing, not missing.
587+
// For example "?foo=" instead of "?foo".
588+
String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue;
589+
590+
encodedValues.add(signatureFormattedEncodedValue);
591+
}
592+
Collections.sort(encodedValues);
593+
sorted.put(encodedParamName, encodedValues);
594+
});
595+
596+
SdkHttpUtils.flattenQueryParameters(result, sorted);
597+
}
598+
491599
public StringBuilder signedHeaderStringBuilder() {
492600
if (signedHeaderStringBuilder == null) {
493601
signedHeaderStringBuilder = new StringBuilder();
@@ -505,7 +613,7 @@ public String signedHeaderString() {
505613

506614
private List<Pair<String, List<String>>> canonicalHeaders() {
507615
if (canonicalHeaders == null) {
508-
canonicalHeaders = canonicalizeSigningHeaders(request);
616+
canonicalHeaders = canonicalizeSigningHeaders(requestBuilder);
509617
}
510618
return canonicalHeaders;
511619
}

core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@
2121
import java.security.DigestInputStream;
2222
import java.security.MessageDigest;
2323
import java.security.NoSuchAlgorithmException;
24-
import java.util.ArrayList;
25-
import java.util.Collections;
26-
import java.util.List;
27-
import java.util.SortedMap;
28-
import java.util.TreeMap;
2924
import javax.crypto.Mac;
3025
import javax.crypto.spec.SecretKeySpec;
3126
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -38,10 +33,8 @@
3833
import software.amazon.awssdk.core.signer.Signer;
3934
import software.amazon.awssdk.http.ContentStreamProvider;
4035
import software.amazon.awssdk.http.SdkHttpFullRequest;
41-
import software.amazon.awssdk.http.SdkHttpRequest;
4236
import software.amazon.awssdk.utils.BinaryUtils;
4337
import software.amazon.awssdk.utils.StringUtils;
44-
import software.amazon.awssdk.utils.http.SdkHttpUtils;
4538

4639
/**
4740
* Abstract base class for AWS signing protocol implementations. Provides
@@ -219,49 +212,6 @@ byte[] hash(byte[] data) throws SdkClientException {
219212
return hash(data, null);
220213
}
221214

222-
/**
223-
* Examines the specified query string parameters and returns a
224-
* canonicalized form.
225-
* <p>
226-
* The canonicalized query string is formed by first sorting all the query
227-
* string parameters, then URI encoding both the key and value and then
228-
* joining them, in order, separating key value pairs with an '&amp;'.
229-
*
230-
* @return A canonicalized form for the specified query string parameters.
231-
*/
232-
protected void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) {
233-
234-
SortedMap<String, List<String>> sorted = new TreeMap<>();
235-
236-
/**
237-
* Signing protocol expects the param values also to be sorted after url
238-
* encoding in addition to sorted parameter names.
239-
*/
240-
httpRequest.forEachRawQueryParameter((key, values) -> {
241-
if (StringUtils.isEmpty(key)) {
242-
// Do not sign empty keys.
243-
return;
244-
}
245-
246-
String encodedParamName = SdkHttpUtils.urlEncode(key);
247-
248-
List<String> encodedValues = new ArrayList<>(values.size());
249-
for (String value : values) {
250-
String encodedValue = SdkHttpUtils.urlEncode(value);
251-
252-
// Null values should be treated as empty for the purposes of signing, not missing.
253-
// For example "?foo=" instead of "?foo".
254-
String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue;
255-
256-
encodedValues.add(signatureFormattedEncodedValue);
257-
}
258-
Collections.sort(encodedValues);
259-
sorted.put(encodedParamName, encodedValues);
260-
});
261-
262-
SdkHttpUtils.flattenQueryParameters(result, sorted);
263-
}
264-
265215
protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider streamProvider) {
266216
try {
267217
if (streamProvider == null) {
@@ -278,31 +228,6 @@ protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider stream
278228
}
279229
}
280230

281-
protected void addCanonicalizedResourcePath(StringBuilder result, String resourcePath, boolean urlEncode) {
282-
if (StringUtils.isEmpty(resourcePath)) {
283-
result.append("/");
284-
} else {
285-
String value = urlEncode ? SdkHttpUtils.urlEncodeIgnoreSlashes(resourcePath) : resourcePath;
286-
if (value.startsWith("/")) {
287-
result.append(value);
288-
} else {
289-
result.append("/").append(value);
290-
}
291-
}
292-
}
293-
294-
protected String getCanonicalizedEndpoint(SdkHttpFullRequest request) {
295-
String endpointForStringToSign = StringUtils.lowerCase(request.host());
296-
297-
// Omit the port from the endpoint if we're using the default port for the protocol. Some HTTP clients (ie. Apache) don't
298-
// allow you to specify it in the request, so we're standardizing around not including it. See SdkHttpRequest#port().
299-
if (!SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())) {
300-
endpointForStringToSign += ":" + request.port();
301-
}
302-
303-
return endpointForStringToSign;
304-
}
305-
306231
/**
307232
* Loads the individual access key ID and secret key from the specified credentials, trimming any extra whitespace from the
308233
* credentials.

0 commit comments

Comments
 (0)