Skip to content

Commit 99e0a5b

Browse files
committed
use json writer
1 parent acd4131 commit 99e0a5b

4 files changed

Lines changed: 272 additions & 24 deletions

File tree

services/cloudfront/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<artifactId>aws-xml-protocol</artifactId>
5252
<version>${awsjavasdk.version}</version>
5353
</dependency>
54+
<dependency>
55+
<groupId>software.amazon.awssdk</groupId>
56+
<artifactId>json-utils</artifactId>
57+
<version>${awsjavasdk.version}</version>
58+
</dependency>
5459
<dependency>
5560
<groupId>software.amazon.awssdk</groupId>
5661
<artifactId>protocol-core</artifactId>

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/internal/utils/SigningUtils.java

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Base64;
3535
import software.amazon.awssdk.annotations.SdkInternalApi;
3636
import software.amazon.awssdk.core.exception.SdkClientException;
37+
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
3738
import software.amazon.awssdk.services.cloudfront.internal.auth.Pem;
3839
import software.amazon.awssdk.utils.IoUtils;
3940
import software.amazon.awssdk.utils.StringUtils;
@@ -57,11 +58,29 @@ private SigningUtils() {
5758
* >Setting signed cookies using a canned policy</a>.
5859
*/
5960
public static String buildCannedPolicy(String resourceUrl, Instant expirationDate) {
60-
return "{\"Statement\":[{\"Resource\":\""
61-
+ resourceUrl
62-
+ "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":"
63-
+ expirationDate.getEpochSecond()
64-
+ "}}}]}";
61+
Validate.notNull(resourceUrl, "resourceUrl must not be null");
62+
Validate.notNull(expirationDate, "expirationDate must not be null");
63+
validateInput(resourceUrl, "resourceUrl");
64+
65+
JsonWriter writer = JsonWriter.create();
66+
writer.writeStartObject()
67+
.writeFieldName("Statement")
68+
.writeStartArray()
69+
.writeStartObject()
70+
.writeFieldName("Resource")
71+
.writeValue(resourceUrl)
72+
.writeFieldName("Condition")
73+
.writeStartObject()
74+
.writeFieldName("DateLessThan")
75+
.writeStartObject()
76+
.writeFieldName("AWS:EpochTime")
77+
.writeValue(expirationDate.getEpochSecond())
78+
.writeEndObject()
79+
.writeEndObject()
80+
.writeEndObject()
81+
.writeEndArray()
82+
.writeEndObject();
83+
return new String(writer.getBytes(), UTF_8);
6584
}
6685

6786
/**
@@ -75,24 +94,72 @@ public static String buildCannedPolicy(String resourceUrl, Instant expirationDat
7594
* >Setting signed cookies using a custom policy</a>.
7695
*/
7796
public static String buildCustomPolicy(String resourceUrl, Instant activeDate, Instant expirationDate,
78-
String ipAddress) {
79-
return "{\"Statement\": [{"
80-
+ "\"Resource\":\""
81-
+ resourceUrl
82-
+ "\""
83-
+ ",\"Condition\":{"
84-
+ "\"DateLessThan\":{\"AWS:EpochTime\":"
85-
+ expirationDate.getEpochSecond()
86-
+ "}"
87-
+ (ipAddress == null
88-
? ""
89-
: ",\"IpAddress\":{\"AWS:SourceIp\":\"" + ipAddress + "\"}"
90-
)
91-
+ (activeDate == null
92-
? ""
93-
: ",\"DateGreaterThan\":{\"AWS:EpochTime\":" + activeDate.getEpochSecond() + "}"
94-
)
95-
+ "}}]}";
97+
String ipAddress) {
98+
Validate.notNull(resourceUrl, "resourceUrl must not be null");
99+
Validate.notNull(expirationDate, "expirationDate must not be null");
100+
validateInput(resourceUrl, "resourceUrl");
101+
if (ipAddress != null) {
102+
validateInput(ipAddress, "ipAddress");
103+
}
104+
105+
JsonWriter writer = JsonWriter.create();
106+
writer.writeStartObject()
107+
.writeFieldName("Statement")
108+
.writeStartArray()
109+
.writeStartObject()
110+
.writeFieldName("Resource")
111+
.writeValue(resourceUrl)
112+
.writeFieldName("Condition")
113+
.writeStartObject()
114+
.writeFieldName("DateLessThan")
115+
.writeStartObject()
116+
.writeFieldName("AWS:EpochTime")
117+
.writeValue(expirationDate.getEpochSecond())
118+
.writeEndObject();
119+
120+
if (ipAddress != null) {
121+
writer.writeFieldName("IpAddress")
122+
.writeStartObject()
123+
.writeFieldName("AWS:SourceIp")
124+
.writeValue(ipAddress)
125+
.writeEndObject();
126+
}
127+
128+
if (activeDate != null) {
129+
writer.writeFieldName("DateGreaterThan")
130+
.writeStartObject()
131+
.writeFieldName("AWS:EpochTime")
132+
.writeValue(activeDate.getEpochSecond())
133+
.writeEndObject();
134+
}
135+
136+
writer.writeEndObject()
137+
.writeEndObject()
138+
.writeEndArray()
139+
.writeEndObject();
140+
141+
return new String(writer.getBytes(), UTF_8);
142+
}
143+
144+
/**
145+
* Validates that the input does not contain characters that could be used for JSON injection attacks.
146+
* Double quotes, backslashes, and control characters should never appear in valid CloudFront resource URLs
147+
* or IP addresses.
148+
*
149+
* @param input the input string to validate
150+
* @param paramName the parameter name for error messages
151+
* @throws IllegalArgumentException if the input contains invalid characters
152+
*/
153+
private static void validateInput(String input, String paramName) {
154+
for (int i = 0; i < input.length(); i++) {
155+
char c = input.charAt(i);
156+
if (c == '"' || c == '\\' || Character.isISOControl(c)) {
157+
throw new IllegalArgumentException(
158+
paramName + " contains invalid characters. The character '" + c + "' at position " + i +
159+
" is not allowed. URLs and IP addresses should be properly encoded and must not contain " +
160+
"double quotes, backslashes, or control characters.");
161+
}
162+
}
96163
}
97164

98165
/**

services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ void getSignedURLWithCustomPolicy_policyResourceUrlShouldHandleVariousPatterns(
429429
StringJoiner conditions = new StringJoiner(",", "{", "}");
430430
conditions.add("\"DateLessThan\":{\"AWS:EpochTime\":" + expiration.getEpochSecond() + "}");
431431

432-
expectedPolicy.append("{\"Statement\": [{")
432+
expectedPolicy.append("{\"Statement\":[{")
433433
.append("\"Resource\":\"").append(expectedResource).append("\",")
434434
.append("\"Condition\":").append(conditions)
435435
.append("}]}");
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.cloudfront.internal.utils;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.time.Instant;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
26+
class SigningUtilsTest {
27+
28+
private static final Instant EXPIRATION = Instant.ofEpochSecond(1704067200);
29+
private static final Instant ACTIVE_DATE = Instant.ofEpochSecond(1640995200);
30+
private static final String VALID_URL = "https://d111111abcdef8.cloudfront.net/s3ObjectKey";
31+
private static final String VALID_IP = "192.168.1.0/24";
32+
33+
@Test
34+
void buildCannedPolicy_withValidUrl_producesValidJson() {
35+
String policy = SigningUtils.buildCannedPolicy(VALID_URL, EXPIRATION);
36+
37+
assertThat(policy).contains("\"Resource\":\"" + VALID_URL + "\"");
38+
assertThat(policy).contains("\"AWS:EpochTime\":" + EXPIRATION.getEpochSecond());
39+
// Verify it's valid JSON structure
40+
assertThat(policy).startsWith("{");
41+
assertThat(policy).endsWith("}");
42+
}
43+
44+
@Test
45+
void buildCustomPolicy_withAllParameters_producesValidJson() {
46+
String policy = SigningUtils.buildCustomPolicy(VALID_URL, ACTIVE_DATE, EXPIRATION, VALID_IP);
47+
48+
assertThat(policy).contains("\"Resource\":\"" + VALID_URL + "\"");
49+
assertThat(policy).contains("\"DateLessThan\"");
50+
assertThat(policy).contains("\"DateGreaterThan\"");
51+
assertThat(policy).contains("\"IpAddress\"");
52+
assertThat(policy).contains("\"AWS:SourceIp\":\"" + VALID_IP + "\"");
53+
}
54+
55+
56+
@Test
57+
void buildCannedPolicy_withDoubleQuoteInUrl_shouldRejectInput() {
58+
String maliciousUrl = "https://example.com/file\",\"Resource\":\"*\",\"x\":\"";
59+
60+
assertThatThrownBy(() -> SigningUtils.buildCannedPolicy(maliciousUrl, EXPIRATION))
61+
.isInstanceOf(IllegalArgumentException.class)
62+
.hasMessageContaining("contains invalid characters")
63+
.hasMessageContaining("resourceUrl");
64+
}
65+
66+
@Test
67+
void buildCannedPolicy_withBackslashInUrl_shouldRejectInput() {
68+
String maliciousUrl = "https://example.com/file\\";
69+
70+
assertThatThrownBy(() -> SigningUtils.buildCannedPolicy(maliciousUrl, EXPIRATION))
71+
.isInstanceOf(IllegalArgumentException.class)
72+
.hasMessageContaining("contains invalid characters");
73+
}
74+
75+
@ParameterizedTest
76+
@ValueSource(strings = {
77+
"https://example.com/file\u0000", // null character
78+
"https://example.com/file\n", // newline
79+
"https://example.com/file\r", // carriage return
80+
"https://example.com/file\t" // tab
81+
})
82+
void buildCannedPolicy_withControlCharactersInUrl_shouldRejectInput(String maliciousUrl) {
83+
assertThatThrownBy(() -> SigningUtils.buildCannedPolicy(maliciousUrl, EXPIRATION))
84+
.isInstanceOf(IllegalArgumentException.class)
85+
.hasMessageContaining("contains invalid characters");
86+
}
87+
88+
@Test
89+
void buildCustomPolicy_withDoubleQuoteInUrl_shouldRejectInput() {
90+
String maliciousUrl = "https://example.com/file\"";
91+
92+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicy(maliciousUrl, ACTIVE_DATE, EXPIRATION, VALID_IP))
93+
.isInstanceOf(IllegalArgumentException.class)
94+
.hasMessageContaining("resourceUrl");
95+
}
96+
97+
@Test
98+
void buildCustomPolicy_withDoubleQuoteInIpAddress_shouldRejectInput() {
99+
String maliciousIp = "192.168.1.0\",\"Resource\":\"*";
100+
101+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicy(VALID_URL, ACTIVE_DATE, EXPIRATION, maliciousIp))
102+
.isInstanceOf(IllegalArgumentException.class)
103+
.hasMessageContaining("ipAddress");
104+
}
105+
106+
@Test
107+
void buildCustomPolicyForSignedUrl_withDoubleQuoteInUrl_shouldRejectInput() {
108+
String maliciousUrl = "https://example.com/file\"";
109+
110+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicyForSignedUrl(maliciousUrl, ACTIVE_DATE, EXPIRATION, VALID_IP))
111+
.isInstanceOf(IllegalArgumentException.class)
112+
.hasMessageContaining("contains invalid characters");
113+
}
114+
115+
@Test
116+
void buildCustomPolicyForSignedUrl_withDoubleQuoteInIpRange_shouldRejectInput() {
117+
String maliciousIp = "192.168.1.0\"";
118+
119+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicyForSignedUrl(VALID_URL, ACTIVE_DATE, EXPIRATION, maliciousIp))
120+
.isInstanceOf(IllegalArgumentException.class)
121+
.hasMessageContaining("contains invalid characters");
122+
}
123+
124+
// Null parameter validation tests
125+
126+
@Test
127+
void buildCannedPolicy_withNullUrl_shouldThrowNullPointerException() {
128+
assertThatThrownBy(() -> SigningUtils.buildCannedPolicy(null, EXPIRATION))
129+
.isInstanceOf(NullPointerException.class)
130+
.hasMessageContaining("resourceUrl");
131+
}
132+
133+
@Test
134+
void buildCannedPolicy_withNullExpiration_shouldThrowNullPointerException() {
135+
assertThatThrownBy(() -> SigningUtils.buildCannedPolicy(VALID_URL, null))
136+
.isInstanceOf(NullPointerException.class)
137+
.hasMessageContaining("expirationDate");
138+
}
139+
140+
@Test
141+
void buildCustomPolicy_withNullUrl_shouldThrowNullPointerException() {
142+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicy(null, ACTIVE_DATE, EXPIRATION, VALID_IP))
143+
.isInstanceOf(NullPointerException.class)
144+
.hasMessageContaining("resourceUrl");
145+
}
146+
147+
@Test
148+
void buildCustomPolicy_withNullExpiration_shouldThrowNullPointerException() {
149+
assertThatThrownBy(() -> SigningUtils.buildCustomPolicy(VALID_URL, ACTIVE_DATE, null, VALID_IP))
150+
.isInstanceOf(NullPointerException.class)
151+
.hasMessageContaining("expirationDate");
152+
}
153+
154+
// Valid edge cases that should still work
155+
156+
@Test
157+
void buildCannedPolicy_withWildcard_shouldSucceed() {
158+
String policy = SigningUtils.buildCannedPolicy("*", EXPIRATION);
159+
assertThat(policy).contains("\"Resource\":\"*\"");
160+
}
161+
162+
@Test
163+
void buildCannedPolicy_withWildcardInPath_shouldSucceed() {
164+
String url = "https://d111111abcdef8.cloudfront.net/*";
165+
String policy = SigningUtils.buildCannedPolicy(url, EXPIRATION);
166+
assertThat(policy).contains("\"Resource\":\"" + url + "\"");
167+
}
168+
169+
@Test
170+
void buildCannedPolicy_withQueryParameters_shouldSucceed() {
171+
String url = "https://d111111abcdef8.cloudfront.net/file?param=value&other=123";
172+
String policy = SigningUtils.buildCannedPolicy(url, EXPIRATION);
173+
assertThat(policy).contains("\"Resource\":\"" + url + "\"");
174+
}
175+
176+
}

0 commit comments

Comments
 (0)