Skip to content

Fixed a bug where path normalization was not being disabled when signing requests directly with AwsS3V4Signer. #3601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AmazonS3-ba24933.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "Amazon S3",
"contributor": "",
"type": "bugfix",
"description": "Fixed an issue that could result in exceptions or signature validation failures when signing or presigning S3 requests using the signer directly when paths contain encoded or path traversal characters."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

package software.amazon.awssdk.auth.signer;

import java.time.Clock;
import java.time.Instant;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
import software.amazon.awssdk.core.interceptor.ExecutionAttribute;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
Expand Down Expand Up @@ -64,6 +66,13 @@ public final class AwsSignerExecutionAttribute extends SdkExecutionAttribute {
public static final ExecutionAttribute<Boolean> SIGNER_NORMALIZE_PATH =
new ExecutionAttribute<>("NormalizePath");


/**
* An override clock to use during signing.
* @see Aws4SignerParams.Builder#signingClockOverride(Clock)
*/
public static final ExecutionAttribute<Clock> SIGNING_CLOCK = new ExecutionAttribute<>("Clock");

/**
* The key to specify the expiration time when pre-signing aws requests.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,8 @@ protected <B extends Aws4SignerParams.Builder> B extractSignerParams(B paramsBui
paramsBuilder.awsCredentials(executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS))
.signingName(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME))
.signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
.timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET));
.timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET))
.signingClockOverride(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_CLOCK));

Boolean doubleUrlEncode = executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE);
if (doubleUrlEncode != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public SdkHttpFullRequest presign(SdkHttpFullRequest request, Aws4PresignerParam
return request;
}

signingParams = signingParams.copy(b -> b.normalizePath(false));

Aws4SignerRequestParams requestParams = new Aws4SignerRequestParams(signingParams);

return doPresign(request, requestParams, signingParams).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
import java.util.Optional;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.auth.signer.internal.SignerConstant;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;

@SdkPublicApi
public final class Aws4PresignerParams extends Aws4SignerParams {
public final class Aws4PresignerParams
extends Aws4SignerParams
implements ToCopyableBuilder<Aws4PresignerParams.Builder, Aws4PresignerParams> {

private final Instant expirationTime;

Expand All @@ -38,7 +42,12 @@ public static Builder builder() {
return new BuilderImpl();
}

public interface Builder extends Aws4SignerParams.Builder<Builder> {
@Override
public Builder toBuilder() {
return new BuilderImpl(this);
}

public interface Builder extends Aws4SignerParams.Builder<Builder>, CopyableBuilder<Builder, Aws4PresignerParams> {

/**
* Sets an expiration time for the presigned url. If this value is not specified,
Expand All @@ -58,6 +67,11 @@ private static final class BuilderImpl extends Aws4SignerParams.BuilderImpl<Buil
private BuilderImpl() {
}

private BuilderImpl(Aws4PresignerParams params) {
super(params);
this.expirationTime = params.expirationTime;
}

@Override
public Builder expirationTime(Instant expirationTime) {
this.expirationTime = expirationTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public SignerChecksumParams checksumParams() {
return checksumParams;
}

public interface Builder<B extends Builder> {
public interface Builder<B extends Builder<B>> {

/**
* Set this value to double url-encode the resource path when constructing the
Expand Down Expand Up @@ -155,7 +155,7 @@ public interface Builder<B extends Builder> {
Aws4SignerParams build();
}

protected static class BuilderImpl<B extends Builder> implements Builder<B> {
protected static class BuilderImpl<B extends Builder<B>> implements Builder<B> {
private static final Boolean DEFAULT_DOUBLE_URL_ENCODE = Boolean.TRUE;

private Boolean doubleUrlEncode = DEFAULT_DOUBLE_URL_ENCODE;
Expand All @@ -168,7 +168,17 @@ protected static class BuilderImpl<B extends Builder> implements Builder<B> {
private SignerChecksumParams checksumParams;

protected BuilderImpl() {
}

protected BuilderImpl(Aws4SignerParams params) {
doubleUrlEncode = params.doubleUrlEncode;
normalizePath = params.normalizePath;
awsCredentials = params.awsCredentials;
signingName = params.signingName;
signingRegion = params.signingRegion;
timeOffset = params.timeOffset;
signingClockOverride = params.signingClockOverride;
checksumParams = params.checksumParams;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ private static final class BuilderImpl extends Aws4SignerParams.BuilderImpl<Buil
private Boolean enablePayloadSigning = DEFAULT_PAYLOAD_SIGNING_ENABLED;

private BuilderImpl() {
// By default, S3 should not normalize paths
normalizePath(false);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* 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;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
import software.amazon.awssdk.auth.signer.params.AwsS3V4SignerParams;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;

class AwsS3V4SignerTest {
private static final Clock UTC_EPOCH_CLOCK = Clock.fixed(Instant.EPOCH, ZoneOffset.UTC);

@Test
public void signWithParams_urlsAreNotNormalized() {
byte[] bytes = new byte[1000];
ThreadLocalRandom.current().nextBytes(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
URI target = URI.create("https://test.com/./foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.contentStreamProvider(RequestBody.fromByteBuffer(buffer)
.contentStreamProvider())
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
AwsS3V4Signer signer = AwsS3V4Signer.create();
SdkHttpFullRequest signedRequest =
signer.sign(request,
AwsS3V4SignerParams.builder()
.awsCredentials(AwsBasicCredentials.create("akid", "skid"))
.signingRegion(Region.US_WEST_2)
.signingName("s3")
.signingClockOverride(UTC_EPOCH_CLOCK)
.build());

assertThat(signedRequest.firstMatchingHeader("Authorization"))
.hasValue("AWS4-HMAC-SHA256 Credential=akid/19700101/us-west-2/s3/aws4_request, "
+ "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
+ "Signature=a3b97f9de337ab254f3b366c3d0b3c67016d2d8d8ba7e0e4ddab0ccebe84992a");
}

@Test
public void signWithExecutionAttributes_urlsAreNotNormalized() {
byte[] bytes = new byte[1000];
ThreadLocalRandom.current().nextBytes(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
URI target = URI.create("https://test.com/./foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.contentStreamProvider(RequestBody.fromByteBuffer(buffer)
.contentStreamProvider())
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
ExecutionAttributes attributes =
ExecutionAttributes.builder()
.put(AwsSignerExecutionAttribute.AWS_CREDENTIALS,
AwsBasicCredentials.create("akid", "skid"))
.put(AwsSignerExecutionAttribute.SIGNING_REGION, Region.US_WEST_2)
.put(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "s3")
.put(AwsSignerExecutionAttribute.SIGNING_CLOCK, UTC_EPOCH_CLOCK)
.build();

AwsS3V4Signer signer = AwsS3V4Signer.create();
SdkHttpFullRequest signedRequest = signer.sign(request, attributes);

assertThat(signedRequest.firstMatchingHeader("Authorization"))
.hasValue("AWS4-HMAC-SHA256 Credential=akid/19700101/us-west-2/s3/aws4_request, "
+ "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
+ "Signature=a3b97f9de337ab254f3b366c3d0b3c67016d2d8d8ba7e0e4ddab0ccebe84992a");
}

@Test
public void presignWithParams_urlsAreNotNormalized() {
byte[] bytes = new byte[1000];
ThreadLocalRandom.current().nextBytes(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
URI target = URI.create("https://test.com/./foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.contentStreamProvider(RequestBody.fromByteBuffer(buffer)
.contentStreamProvider())
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
AwsS3V4Signer signer = AwsS3V4Signer.create();

SdkHttpFullRequest signedRequest =
signer.presign(request,
Aws4PresignerParams.builder()
.awsCredentials(AwsBasicCredentials.create("akid", "skid"))
.signingRegion(Region.US_WEST_2)
.signingName("s3")
.signingClockOverride(UTC_EPOCH_CLOCK)
.build());

assertThat(signedRequest.firstMatchingRawQueryParameter("X-Amz-Signature"))
.hasValue("3a9d36d37e9a554b7a3803f58ee7539b5d1f52fdfe89ce6fd40fb25762a35ec3");
}

@Test
public void presignWithExecutionAttributes_urlsAreNotNormalized() {
byte[] bytes = new byte[1000];
ThreadLocalRandom.current().nextBytes(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
URI target = URI.create("https://test.com/./foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.contentStreamProvider(RequestBody.fromByteBuffer(buffer)
.contentStreamProvider())
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
ExecutionAttributes attributes =
ExecutionAttributes.builder()
.put(AwsSignerExecutionAttribute.AWS_CREDENTIALS,
AwsBasicCredentials.create("akid", "skid"))
.put(AwsSignerExecutionAttribute.SIGNING_REGION, Region.US_WEST_2)
.put(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "s3")
.put(AwsSignerExecutionAttribute.SIGNING_CLOCK, UTC_EPOCH_CLOCK)
.build();

AwsS3V4Signer signer = AwsS3V4Signer.create();
SdkHttpFullRequest signedRequest = signer.presign(request, attributes);

assertThat(signedRequest.firstMatchingRawQueryParameter("X-Amz-Signature"))
.hasValue("3a9d36d37e9a554b7a3803f58ee7539b5d1f52fdfe89ce6fd40fb25762a35ec3");
}

@Test
public void signWithParams_doesNotFailWithEncodedCharacters() {
URI target = URI.create("https://test.com/%20foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
AwsS3V4Signer signer = AwsS3V4Signer.create();
assertDoesNotThrow(() ->
signer.sign(request,
AwsS3V4SignerParams.builder()
.awsCredentials(AwsBasicCredentials.create("akid", "skid"))
.signingRegion(Region.US_WEST_2)
.signingName("s3")
.build()));
}

@Test
public void signWithExecutionAttributes_doesNotFailWithEncodedCharacters() {
URI target = URI.create("https://test.com/%20foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
ExecutionAttributes attributes =
ExecutionAttributes.builder()
.put(AwsSignerExecutionAttribute.AWS_CREDENTIALS,
AwsBasicCredentials.create("akid", "skid"))
.put(AwsSignerExecutionAttribute.SIGNING_REGION, Region.US_WEST_2)
.put(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "s3")
.build();

AwsS3V4Signer signer = AwsS3V4Signer.create();
assertDoesNotThrow(() -> signer.sign(request, attributes));
}

@Test
public void presignWithParams_doesNotFailWithEncodedCharacters() {
URI target = URI.create("https://test.com/%20foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
AwsS3V4Signer signer = AwsS3V4Signer.create();

assertDoesNotThrow(() ->
signer.presign(request,
Aws4PresignerParams.builder()
.awsCredentials(AwsBasicCredentials.create("akid", "skid"))
.signingRegion(Region.US_WEST_2)
.signingName("s3")
.build()));
}

@Test
public void presignWithExecutionAttributes_doesNotFailWithEncodedCharacters() {
URI target = URI.create("https://test.com/%20foo");

SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.uri(target)
.encodedPath(target.getPath())
.build();
ExecutionAttributes attributes =
ExecutionAttributes.builder()
.put(AwsSignerExecutionAttribute.AWS_CREDENTIALS,
AwsBasicCredentials.create("akid", "skid"))
.put(AwsSignerExecutionAttribute.SIGNING_REGION, Region.US_WEST_2)
.put(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, "s3")
.build();

AwsS3V4Signer signer = AwsS3V4Signer.create();
assertDoesNotThrow(() -> signer.presign(request, attributes));
}
}