From d593fdd23d3b3e00fa4fec327e43c5def769754d Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 26 May 2021 15:38:12 -0700 Subject: [PATCH] Normalize URLs during signing (except for S3). 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. --- .../bugfix-AWSSDKforJavav2-d06ff61.json | 6 + .../signer/AwsSignerExecutionAttribute.java | 6 + .../signer/internal/AbstractAws4Signer.java | 142 +++++++++++++++--- .../signer/internal/AbstractAwsSigner.java | 75 --------- .../auth/signer/params/Aws4SignerParams.java | 27 +++- .../Aws4SignerPathNormalizationTest.java | 111 ++++++++++++++ .../apigateway/ServiceIntegrationTest.java | 16 ++ ...r.java => ConfigureSignerInterceptor.java} | 7 +- .../s3/reflect-config.json | 2 +- .../awssdk/services/s3/execution.interceptors | 2 +- ...r.java => ConfigureSignerInterceptor.java} | 9 +- .../s3control/reflect-config.json | 2 +- .../services/s3control/execution.interceptors | 2 +- 13 files changed, 302 insertions(+), 105 deletions(-) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-d06ff61.json create mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/signer/internal/Aws4SignerPathNormalizationTest.java rename services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/{DisableDoubleUrlEncodingInterceptor.java => ConfigureSignerInterceptor.java} (78%) rename services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/{DisableDoubleUrlEncodingForSigningInterceptor.java => ConfigureSignerInterceptor.java} (76%) diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-d06ff61.json b/.changes/next-release/bugfix-AWSSDKforJavav2-d06ff61.json new file mode 100644 index 000000000000..2b2642ba89e0 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-d06ff61.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fixed an issue that could result in signature mismatch exceptions when requests included . or .." +} diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java index ec4afb2552e4..f32072be4057 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java @@ -58,6 +58,12 @@ public final class AwsSignerExecutionAttribute extends SdkExecutionAttribute { */ public static final ExecutionAttribute SIGNER_DOUBLE_URL_ENCODE = new ExecutionAttribute<>("DoubleUrlEncode"); + /** + * The key to specify whether to normalize the resource path during signing. + */ + public static final ExecutionAttribute SIGNER_NORMALIZE_PATH = + new ExecutionAttribute<>("NormalizePath"); + /** * The key to specify the expiration time when pre-signing aws requests. */ diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java index 4c81d083727b..472b6845e758 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java @@ -24,8 +24,11 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; @@ -41,6 +44,7 @@ import software.amazon.awssdk.core.internal.util.HttpChecksumUtils; import software.amazon.awssdk.core.signer.Presigner; import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.utils.BinaryUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.Pair; @@ -81,6 +85,7 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request, ContentChecksum contentChecksum) { SdkHttpFullRequest.Builder mutableRequest = request.toBuilder(); + AwsCredentials sanitizedCredentials = sanitizeCredentials(signingParams.awsCredentials()); if (sanitizedCredentials instanceof AwsSessionCredentials) { addSessionCredentials(mutableRequest, (AwsSessionCredentials) sanitizedCredentials); @@ -97,9 +102,11 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request, putChecksumHeader(signingParams.checksumParams(), contentChecksum.contentFlexibleChecksum(), mutableRequest, contentChecksum.contentHash()); - CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest, + CanonicalRequest canonicalRequest = createCanonicalRequest(request, + mutableRequest, contentChecksum.contentHash(), - signingParams.doubleUrlEncode()); + signingParams.doubleUrlEncode(), + signingParams.normalizePath()); String canonicalRequestString = canonicalRequest.string(); String stringToSign = createStringToSign(canonicalRequestString, requestParams); @@ -138,8 +145,11 @@ protected SdkHttpFullRequest.Builder doPresign(SdkHttpFullRequest request, // Add the important parameters for v4 signing String contentSha256 = calculateContentHashPresign(mutableRequest, signingParams); - CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest, contentSha256, - signingParams.doubleUrlEncode()); + CanonicalRequest canonicalRequest = createCanonicalRequest(request, + mutableRequest, + contentSha256, + signingParams.doubleUrlEncode(), + signingParams.normalizePath()); addPreSignInformationToRequest(mutableRequest, canonicalRequest, sanitizedCredentials, requestParams, expirationInSeconds); @@ -237,10 +247,12 @@ protected final byte[] deriveSigningKey(AwsCredentials credentials, Instant sign * .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to * generate the canonical request. */ - private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder request, + private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest request, + SdkHttpFullRequest.Builder requestBuilder, String contentSha256, - boolean doubleUrlEncode) { - return new CanonicalRequest(request, contentSha256, doubleUrlEncode); + boolean doubleUrlEncode, + boolean normalizePath) { + return new CanonicalRequest(request, requestBuilder, contentSha256, doubleUrlEncode, normalizePath); } /** @@ -251,6 +263,8 @@ private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder reque private String createStringToSign(String canonicalRequest, Aws4SignerRequestParams requestParams) { + LOG.debug(() -> "AWS4 Canonical Request: " + canonicalRequest); + String requestHash = BinaryUtils.toHex(hash(canonicalRequest)); String stringToSign = requestParams.getSigningAlgorithm() + @@ -330,7 +344,7 @@ private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRe * @param ch the character to be tested * @return true if the character is white space, false otherwise. */ - private boolean isWhiteSpace(final char ch) { + private static boolean isWhiteSpace(char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f'; } @@ -402,8 +416,15 @@ protected B extractSignerParams(B paramsBui .signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION)) .timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET)); - if (executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE) != null) { - paramsBuilder.doubleUrlEncode(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE)); + Boolean doubleUrlEncode = executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE); + if (doubleUrlEncode != null) { + paramsBuilder.doubleUrlEncode(doubleUrlEncode); + } + + Boolean normalizePath = + executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH); + if (normalizePath != null) { + paramsBuilder.normalizePath(normalizePath); } ChecksumSpecs checksumSpecs = executionAttributes.getAttribute(RESOLVED_CHECKSUM_SPECS); if (checksumSpecs != null && checksumSpecs.algorithm() != null) { @@ -453,30 +474,41 @@ private SdkChecksum createSdkChecksumFromParams(T signingParams, SdkHttpFullRequ return null; } - private class CanonicalRequest { - private final SdkHttpFullRequest.Builder request; + static final class CanonicalRequest { + private final SdkHttpFullRequest request; + private final SdkHttpFullRequest.Builder requestBuilder; private final String contentSha256; private final boolean doubleUrlEncode; + private final boolean normalizePath; private String canonicalRequestString; private StringBuilder signedHeaderStringBuilder; private List>> canonicalHeaders; private String signedHeaderString; - private CanonicalRequest(SdkHttpFullRequest.Builder request, String contentSha256, boolean doubleUrlEncode) { + CanonicalRequest(SdkHttpFullRequest request, + SdkHttpFullRequest.Builder requestBuilder, + String contentSha256, + boolean doubleUrlEncode, + boolean normalizePath) { this.request = request; + this.requestBuilder = requestBuilder; this.contentSha256 = contentSha256; this.doubleUrlEncode = doubleUrlEncode; + this.normalizePath = normalizePath; } public String string() { if (canonicalRequestString == null) { StringBuilder canonicalRequest = new StringBuilder(512); - canonicalRequest.append(request.method().toString()) + canonicalRequest.append(requestBuilder.method().toString()) .append(SignerConstant.LINE_SEPARATOR); - addCanonicalizedResourcePath(canonicalRequest, request.encodedPath(), doubleUrlEncode); + addCanonicalizedResourcePath(canonicalRequest, + request, + doubleUrlEncode, + normalizePath); canonicalRequest.append(SignerConstant.LINE_SEPARATOR); - addCanonicalizedQueryString(canonicalRequest, request); + addCanonicalizedQueryString(canonicalRequest, requestBuilder); canonicalRequest.append(SignerConstant.LINE_SEPARATOR); addCanonicalizedHeaderString(canonicalRequest, canonicalHeaders()); canonicalRequest.append(SignerConstant.LINE_SEPARATOR) @@ -488,6 +520,82 @@ public String string() { return canonicalRequestString; } + private void addCanonicalizedResourcePath(StringBuilder result, + SdkHttpRequest request, + boolean urlEncode, + boolean normalizePath) { + String path = normalizePath ? request.getUri().normalize().getRawPath() + : request.encodedPath(); + + if (StringUtils.isEmpty(path)) { + result.append("/"); + return; + } + + if (urlEncode) { + path = SdkHttpUtils.urlEncodeIgnoreSlashes(path); + } + + if (!path.startsWith("/")) { + result.append("/"); + } + result.append(path); + + // Normalization can leave a trailing slash at the end of the resource path, + // even if the input path doesn't end with one. Example input: /foo/bar/. + // Remove the trailing slash if the input path doesn't end with one. + boolean trimTrailingSlash = normalizePath && + path.length() > 1 && + !request.encodedPath().endsWith("/") && + result.charAt(result.length() - 1) == '/'; + if (trimTrailingSlash) { + result.setLength(result.length() - 1); + } + } + + /** + * Examines the specified query string parameters and returns a + * canonicalized form. + *

+ * The canonicalized query string is formed by first sorting all the query + * string parameters, then URI encoding both the key and value and then + * joining them, in order, separating key value pairs with an '&'. + * + * @return A canonicalized form for the specified query string parameters. + */ + private void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) { + + SortedMap> sorted = new TreeMap<>(); + + /** + * Signing protocol expects the param values also to be sorted after url + * encoding in addition to sorted parameter names. + */ + httpRequest.forEachRawQueryParameter((key, values) -> { + if (StringUtils.isEmpty(key)) { + // Do not sign empty keys. + return; + } + + String encodedParamName = SdkHttpUtils.urlEncode(key); + + List encodedValues = new ArrayList<>(values.size()); + for (String value : values) { + String encodedValue = SdkHttpUtils.urlEncode(value); + + // Null values should be treated as empty for the purposes of signing, not missing. + // For example "?foo=" instead of "?foo". + String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue; + + encodedValues.add(signatureFormattedEncodedValue); + } + Collections.sort(encodedValues); + sorted.put(encodedParamName, encodedValues); + }); + + SdkHttpUtils.flattenQueryParameters(result, sorted); + } + public StringBuilder signedHeaderStringBuilder() { if (signedHeaderStringBuilder == null) { signedHeaderStringBuilder = new StringBuilder(); @@ -505,7 +613,7 @@ public String signedHeaderString() { private List>> canonicalHeaders() { if (canonicalHeaders == null) { - canonicalHeaders = canonicalizeSigningHeaders(request); + canonicalHeaders = canonicalizeSigningHeaders(requestBuilder); } return canonicalHeaders; } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java index 65b321c19361..d79e0244bb49 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java @@ -21,11 +21,6 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -38,10 +33,8 @@ import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.http.ContentStreamProvider; import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.utils.BinaryUtils; import software.amazon.awssdk.utils.StringUtils; -import software.amazon.awssdk.utils.http.SdkHttpUtils; /** * Abstract base class for AWS signing protocol implementations. Provides @@ -219,49 +212,6 @@ byte[] hash(byte[] data) throws SdkClientException { return hash(data, null); } - /** - * Examines the specified query string parameters and returns a - * canonicalized form. - *

- * The canonicalized query string is formed by first sorting all the query - * string parameters, then URI encoding both the key and value and then - * joining them, in order, separating key value pairs with an '&'. - * - * @return A canonicalized form for the specified query string parameters. - */ - protected void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) { - - SortedMap> sorted = new TreeMap<>(); - - /** - * Signing protocol expects the param values also to be sorted after url - * encoding in addition to sorted parameter names. - */ - httpRequest.forEachRawQueryParameter((key, values) -> { - if (StringUtils.isEmpty(key)) { - // Do not sign empty keys. - return; - } - - String encodedParamName = SdkHttpUtils.urlEncode(key); - - List encodedValues = new ArrayList<>(values.size()); - for (String value : values) { - String encodedValue = SdkHttpUtils.urlEncode(value); - - // Null values should be treated as empty for the purposes of signing, not missing. - // For example "?foo=" instead of "?foo". - String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue; - - encodedValues.add(signatureFormattedEncodedValue); - } - Collections.sort(encodedValues); - sorted.put(encodedParamName, encodedValues); - }); - - SdkHttpUtils.flattenQueryParameters(result, sorted); - } - protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider streamProvider) { try { if (streamProvider == null) { @@ -278,31 +228,6 @@ protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider stream } } - protected void addCanonicalizedResourcePath(StringBuilder result, String resourcePath, boolean urlEncode) { - if (StringUtils.isEmpty(resourcePath)) { - result.append("/"); - } else { - String value = urlEncode ? SdkHttpUtils.urlEncodeIgnoreSlashes(resourcePath) : resourcePath; - if (value.startsWith("/")) { - result.append(value); - } else { - result.append("/").append(value); - } - } - } - - protected String getCanonicalizedEndpoint(SdkHttpFullRequest request) { - String endpointForStringToSign = StringUtils.lowerCase(request.host()); - - // Omit the port from the endpoint if we're using the default port for the protocol. Some HTTP clients (ie. Apache) don't - // allow you to specify it in the request, so we're standardizing around not including it. See SdkHttpRequest#port(). - if (!SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())) { - endpointForStringToSign += ":" + request.port(); - } - - return endpointForStringToSign; - } - /** * Loads the individual access key ID and secret key from the specified credentials, trimming any extra whitespace from the * credentials. diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/params/Aws4SignerParams.java b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/params/Aws4SignerParams.java index 0e9b00ba5da0..cf0c279dd7aa 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/signer/params/Aws4SignerParams.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/signer/params/Aws4SignerParams.java @@ -31,6 +31,7 @@ @SdkPublicApi public class Aws4SignerParams { private final Boolean doubleUrlEncode; + private final Boolean normalizePath; private final AwsCredentials awsCredentials; private final String signingName; private final Region signingRegion; @@ -39,7 +40,8 @@ public class Aws4SignerParams { private final SignerChecksumParams checksumParams; Aws4SignerParams(BuilderImpl builder) { - this.doubleUrlEncode = Validate.paramNotNull(builder.doubleUrlEncode, "Double Url encode"); + this.doubleUrlEncode = Validate.paramNotNull(builder.doubleUrlEncode, "Double url encode"); + this.normalizePath = Validate.paramNotNull(builder.normalizePath, "Normalize resource path"); this.awsCredentials = Validate.paramNotNull(builder.awsCredentials, "Credentials"); this.signingName = Validate.paramNotNull(builder.signingName, "service signing name"); this.signingRegion = Validate.paramNotNull(builder.signingRegion, "signing region"); @@ -56,6 +58,10 @@ public Boolean doubleUrlEncode() { return doubleUrlEncode; } + public Boolean normalizePath() { + return normalizePath; + } + public AwsCredentials awsCredentials() { return awsCredentials; } @@ -92,6 +98,14 @@ public interface Builder { */ B doubleUrlEncode(Boolean doubleUrlEncode); + /** + * Whether the resource path should be "normalized" according to RFC3986 when + * constructing the canonical request. + * + * By default, all services except S3 enable resource path normalization. + */ + B normalizePath(Boolean normalizePath); + /** * Sets the aws credentials to use for computing the signature. * @@ -145,6 +159,7 @@ protected static class BuilderImpl implements Builder { private static final Boolean DEFAULT_DOUBLE_URL_ENCODE = Boolean.TRUE; private Boolean doubleUrlEncode = DEFAULT_DOUBLE_URL_ENCODE; + private Boolean normalizePath = Boolean.TRUE; private AwsCredentials awsCredentials; private String signingName; private Region signingRegion; @@ -166,6 +181,16 @@ public void setDoubleUrlEncode(Boolean doubleUrlEncode) { doubleUrlEncode(doubleUrlEncode); } + @Override + public B normalizePath(Boolean normalizePath) { + this.normalizePath = normalizePath; + return (B) this; + } + + public void setNormalizePath(Boolean normalizePath) { + normalizePath(normalizePath); + } + @Override public B awsCredentials(AwsCredentials awsCredentials) { this.awsCredentials = awsCredentials; diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/signer/internal/Aws4SignerPathNormalizationTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/signer/internal/Aws4SignerPathNormalizationTest.java new file mode 100644 index 000000000000..f889d36c9d81 --- /dev/null +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/signer/internal/Aws4SignerPathNormalizationTest.java @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.auth.signer.internal; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.signer.internal.AbstractAws4Signer.CanonicalRequest; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.utils.ToString; + +/** + * Tests how canonical resource paths are created including normalization + */ +public class Aws4SignerPathNormalizationTest { + public static Iterable data() { + return Arrays.asList( + // Handling slash + tc("Empty path -> (initial) slash added", "", "/"), + tc("Slash -> unchanged", "/", "/"), + tc("Single segment with initial slash -> unchanged", "/foo", "/foo"), + tc("Single segment no slash -> slash prepended", "foo", "/foo"), + tc("Multiple segments -> unchanged", "/foo/bar", "/foo/bar"), + tc("Multiple segments with trailing slash -> unchanged", "/foo/bar/", "/foo/bar/"), + + // Double URL encoding + tc("Multiple segments, urlEncoded slash -> encodes percent", "/foo%2Fbar", "/foo%252Fbar", true, true), + + // No double-url-encoding + normalization + tc("Single segment, dot -> should remove dot", "/.", "/"), + tc("Single segment, double dot -> unchanged", "/..", "/.."), + tc("Multiple segments with dot -> should remove dot", "/foo/./bar", "/foo/bar"), + tc("Multiple segments with ending dot -> should remove dot and trailing slash", "/foo/bar/.", "/foo/bar"), + tc("Multiple segments with dots -> should remove dots and preceding segment", "/foo/bar/../baz", "/foo/baz"), + tc("First segment has colon -> unchanged, url encoded first", "foo:/bar", "/foo%3A/bar", true, true), + + // Double-url-encoding + normalization + tc("Multiple segments, urlEncoded slash -> encodes percent", "/foo%2F.%2Fbar", "/foo%252F.%252Fbar", true, true), + + // Double-url-encoding + no normalization + tc("No url encode, Multiple segments with dot -> unchanged", "/foo/./bar", "/foo/./bar", false, false), + tc("Multiple segments with dots -> unchanged", "/foo/bar/../baz", "/foo/bar/../baz", false, false) + ); + } + + @ParameterizedTest + @MethodSource("data") + public void verifyNormalizedPath(TestCase tc) { + String canonicalRequest = tc.canonicalRequest.string(); + String[] requestParts = canonicalRequest.split("\\n"); + String canonicalPath = requestParts[1]; + assertEquals(tc.expectedPath, canonicalPath); + } + + private static TestCase tc(String name, String path, String expectedPath) { + return new TestCase(name, path, expectedPath, false, true); + } + + private static TestCase tc(String name, String path, String expectedPath, boolean urlEncode, boolean normalizePath) { + return new TestCase(name, path, expectedPath, urlEncode, normalizePath); + } + + private static class TestCase { + private String name; + private String path; + private String expectedPath; + private CanonicalRequest canonicalRequest; + + public TestCase(String name, + String path, + String expectedPath, + boolean urlEncode, + boolean normalizePath) { + SdkHttpFullRequest request = SdkHttpFullRequest.builder() + .protocol("https") + .host("localhost") + .encodedPath(path) + .method(SdkHttpMethod.PUT) + .build(); + this.name = name; + this.path = path; + this.expectedPath = expectedPath; + this.canonicalRequest = new CanonicalRequest(request, request.toBuilder(), "sha-256", urlEncode, normalizePath); + } + + @Override + public String toString() { + return ToString.builder("TestCase") + .add("name", name) + .add("path", path) + .add("expectedPath", expectedPath) + .build(); + } + } +} diff --git a/services/apigateway/src/it/java/software/amazon/awssdk/services/apigateway/ServiceIntegrationTest.java b/services/apigateway/src/it/java/software/amazon/awssdk/services/apigateway/ServiceIntegrationTest.java index 4c5017afe65c..5c21fee1c53a 100644 --- a/services/apigateway/src/it/java/software/amazon/awssdk/services/apigateway/ServiceIntegrationTest.java +++ b/services/apigateway/src/it/java/software/amazon/awssdk/services/apigateway/ServiceIntegrationTest.java @@ -40,6 +40,7 @@ import software.amazon.awssdk.services.apigateway.model.GetRestApiRequest; import software.amazon.awssdk.services.apigateway.model.GetRestApiResponse; import software.amazon.awssdk.services.apigateway.model.IntegrationType; +import software.amazon.awssdk.services.apigateway.model.NotFoundException; import software.amazon.awssdk.services.apigateway.model.Op; import software.amazon.awssdk.services.apigateway.model.PatchOperation; import software.amazon.awssdk.services.apigateway.model.PutIntegrationRequest; @@ -262,4 +263,19 @@ public void testResourceOperations() { Assert.assertEquals(putIntegrationResult.type(), IntegrationType.MOCK); } + + @Test(expected = NotFoundException.class) + public void resourceWithDotSignedCorrectly() { + apiGateway.createResource(r -> r.restApiId(restApiId).pathPart("fooPath").parentId(".")); + } + + @Test(expected = NotFoundException.class) + public void resourceWithDoubleDotSignedCorrectly() { + apiGateway.createResource(r -> r.restApiId(restApiId).pathPart("fooPath").parentId("..")); + } + + @Test(expected = NotFoundException.class) + public void resourceWithEncodedCharactersSignedCorrectly() { + apiGateway.createResource(r -> r.restApiId(restApiId).pathPart("fooPath").parentId("foo/../bar")); + } } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/DisableDoubleUrlEncodingInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ConfigureSignerInterceptor.java similarity index 78% rename from services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/DisableDoubleUrlEncodingInterceptor.java rename to services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ConfigureSignerInterceptor.java index 211a44719e7b..859fdfba9678 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/DisableDoubleUrlEncodingInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ConfigureSignerInterceptor.java @@ -22,14 +22,15 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; /** - * Don't double-url-encode path elements for S3. S3 expects path elements to be encoded only once in - * the canonical URI. + * Don't double-url-encode or normalize path elements for S3, as per + * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */ @SdkInternalApi -public final class DisableDoubleUrlEncodingInterceptor implements ExecutionInterceptor { +public final class ConfigureSignerInterceptor implements ExecutionInterceptor { @Override public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE, Boolean.FALSE); + executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH, Boolean.FALSE); } } diff --git a/services/s3/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json b/services/s3/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json index fe9f2b3e060b..4ab8c986634d 100644 --- a/services/s3/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json +++ b/services/s3/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json @@ -36,7 +36,7 @@ ] }, { - "name": "software.amazon.awssdk.services.s3.internal.handlers.DisableDoubleUrlEncodingInterceptor", + "name": "software.amazon.awssdk.services.s3.internal.handlers.ConfigureSignerInterceptor", "methods": [ { "name": "", diff --git a/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors b/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors index 4df6a4b26c55..89187de9fb91 100644 --- a/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors +++ b/services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors @@ -2,7 +2,7 @@ software.amazon.awssdk.services.s3.internal.handlers.CreateBucketInterceptor software.amazon.awssdk.services.s3.internal.handlers.PutObjectInterceptor software.amazon.awssdk.services.s3.internal.handlers.CreateMultipartUploadRequestInterceptor software.amazon.awssdk.services.s3.internal.handlers.EnableChunkedEncodingInterceptor -software.amazon.awssdk.services.s3.internal.handlers.DisableDoubleUrlEncodingInterceptor +software.amazon.awssdk.services.s3.internal.handlers.ConfigureSignerInterceptor software.amazon.awssdk.services.s3.internal.handlers.DecodeUrlEncodedResponseInterceptor software.amazon.awssdk.services.s3.internal.handlers.GetBucketPolicyInterceptor software.amazon.awssdk.services.s3.internal.handlers.AsyncChecksumValidationInterceptor diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/DisableDoubleUrlEncodingForSigningInterceptor.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/ConfigureSignerInterceptor.java similarity index 76% rename from services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/DisableDoubleUrlEncodingForSigningInterceptor.java rename to services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/ConfigureSignerInterceptor.java index 01c8cbcbbc38..43ed08fdb746 100644 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/DisableDoubleUrlEncodingForSigningInterceptor.java +++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/internal/interceptors/ConfigureSignerInterceptor.java @@ -22,16 +22,15 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; /** - * Execution interceptor which modifies the HTTP request to S3 Control to - * add a signer attribute that will instruct the signer to not double-url-encode path elements. - * S3 Control expects path elements to be encoded only once in the canonical URI. - * Similar functionality exists for S3. + * Don't double-url-encode or normalize path elements for S3, as per + * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */ @SdkInternalApi -public final class DisableDoubleUrlEncodingForSigningInterceptor implements ExecutionInterceptor { +public final class ConfigureSignerInterceptor implements ExecutionInterceptor { @Override public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE, Boolean.FALSE); + executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH, Boolean.FALSE); } } diff --git a/services/s3control/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3control/reflect-config.json b/services/s3control/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3control/reflect-config.json index 712b888c8601..1883564e1139 100644 --- a/services/s3control/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3control/reflect-config.json +++ b/services/s3control/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3control/reflect-config.json @@ -1,6 +1,6 @@ [ { - "name": "software.amazon.awssdk.services.s3control.internal.interceptors.DisableDoubleUrlEncodingForSigningInterceptor", + "name": "software.amazon.awssdk.services.s3control.internal.interceptors.ConfigureSignerInterceptor", "methods": [ { "name": "", diff --git a/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors b/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors index 7bdfaddef292..c601441350d8 100644 --- a/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors +++ b/services/s3control/src/main/resources/software/amazon/awssdk/services/s3control/execution.interceptors @@ -1,2 +1,2 @@ -software.amazon.awssdk.services.s3control.internal.interceptors.DisableDoubleUrlEncodingForSigningInterceptor +software.amazon.awssdk.services.s3control.internal.interceptors.ConfigureSignerInterceptor software.amazon.awssdk.services.s3control.internal.interceptors.PayloadSigningInterceptor \ No newline at end of file