Skip to content

Commit e6abc1f

Browse files
authored
Merge pull request #1056 from 2012160085/main
Update cookie handling to align with rfc6265 specifications
2 parents 5d4a231 + dfcc006 commit e6abc1f

File tree

7 files changed

+374
-50
lines changed

7 files changed

+374
-50
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package com.amazonaws.serverless.proxy.internal.servlet;
2+
3+
import com.amazonaws.serverless.proxy.internal.SecurityUtils;
4+
import jakarta.servlet.http.Cookie;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import java.time.Instant;
8+
import java.time.ZoneId;
9+
import java.time.format.DateTimeFormatter;
10+
import java.util.*;
11+
12+
/**
13+
* Implementation of the CookieProcessor interface that provides cookie parsing and generation functionality.
14+
*/
15+
public class AwsCookieProcessor implements CookieProcessor {
16+
17+
// Cookie attribute constants
18+
static final String COOKIE_COMMENT_ATTR = "Comment";
19+
static final String COOKIE_DOMAIN_ATTR = "Domain";
20+
static final String COOKIE_EXPIRES_ATTR = "Expires";
21+
static final String COOKIE_MAX_AGE_ATTR = "Max-Age";
22+
static final String COOKIE_PATH_ATTR = "Path";
23+
static final String COOKIE_SECURE_ATTR = "Secure";
24+
static final String COOKIE_HTTP_ONLY_ATTR = "HttpOnly";
25+
static final String COOKIE_SAME_SITE_ATTR = "SameSite";
26+
static final String COOKIE_PARTITIONED_ATTR = "Partitioned";
27+
static final String EMPTY_STRING = "";
28+
29+
// BitSet to store valid token characters as defined in RFC 2616
30+
static final BitSet tokenValid = createTokenValidSet();
31+
32+
// BitSet to validate domain characters
33+
static final BitSet domainValid = createDomainValidSet();
34+
35+
static final DateTimeFormatter COOKIE_DATE_FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneId.of("GMT"));
36+
37+
static final String ANCIENT_DATE = COOKIE_DATE_FORMATTER.format(Instant.ofEpochMilli(10000));
38+
39+
static BitSet createTokenValidSet() {
40+
BitSet tokenSet = new BitSet(128);
41+
for (char c = '0'; c <= '9'; c++) tokenSet.set(c);
42+
for (char c = 'a'; c <= 'z'; c++) tokenSet.set(c);
43+
for (char c = 'A'; c <= 'Z'; c++) tokenSet.set(c);
44+
for (char c : "!#$%&'*+-.^_`|~".toCharArray()) tokenSet.set(c);
45+
return tokenSet;
46+
}
47+
48+
static BitSet createDomainValidSet() {
49+
BitSet domainValid = new BitSet(128);
50+
for (char c = '0'; c <= '9'; c++) domainValid.set(c);
51+
for (char c = 'a'; c <= 'z'; c++) domainValid.set(c);
52+
for (char c = 'A'; c <= 'Z'; c++) domainValid.set(c);
53+
domainValid.set('.');
54+
domainValid.set('-');
55+
return domainValid;
56+
}
57+
58+
private final Logger log = LoggerFactory.getLogger(AwsCookieProcessor.class);
59+
60+
@Override
61+
public Cookie[] parseCookieHeader(String cookieHeader) {
62+
// Return an empty array if the input is null or empty after trimming
63+
if (cookieHeader == null || cookieHeader.trim().isEmpty()) {
64+
return new Cookie[0];
65+
}
66+
67+
// Parse cookie header and convert to Cookie array
68+
return Arrays.stream(cookieHeader.split("\\s*;\\s*"))
69+
.map(this::parseCookiePair)
70+
.filter(Objects::nonNull) // Filter out invalid pairs
71+
.toArray(Cookie[]::new);
72+
}
73+
74+
/**
75+
* Parse a single cookie pair (name=value).
76+
*
77+
* @param cookiePair The cookie pair string.
78+
* @return A valid Cookie object or null if the pair is invalid.
79+
*/
80+
private Cookie parseCookiePair(String cookiePair) {
81+
String[] kv = cookiePair.split("=", 2);
82+
83+
if (kv.length != 2) {
84+
log.warn("Ignoring invalid cookie: {}", cookiePair);
85+
return null; // Skip malformed cookie pairs
86+
}
87+
88+
String cookieName = kv[0];
89+
String cookieValue = kv[1];
90+
91+
// Validate name and value
92+
if (!isToken(cookieName)){
93+
log.warn("Ignoring cookie with invalid name: {}={}", cookieName, cookieValue);
94+
return null; // Skip invalid cookie names
95+
}
96+
97+
if (!isValidCookieValue(cookieValue)) {
98+
log.warn("Ignoring cookie with invalid value: {}={}", cookieName, cookieValue);
99+
return null; // Skip invalid cookie values
100+
}
101+
102+
// Return a new Cookie object after security processing
103+
return new Cookie(SecurityUtils.crlf(cookieName), SecurityUtils.crlf(cookieValue));
104+
}
105+
106+
@Override
107+
public String generateHeader(Cookie cookie) {
108+
StringBuilder header = new StringBuilder();
109+
header.append(cookie.getName()).append('=');
110+
111+
String value = cookie.getValue();
112+
if (value != null && value.length() > 0) {
113+
validateCookieValue(value);
114+
header.append(value);
115+
}
116+
117+
int maxAge = cookie.getMaxAge();
118+
if (maxAge == 0) {
119+
appendAttribute(header, COOKIE_EXPIRES_ATTR, ANCIENT_DATE);
120+
} else if (maxAge > 0){
121+
Instant expiresAt = Instant.now().plusSeconds(maxAge);
122+
appendAttribute(header, COOKIE_EXPIRES_ATTR, COOKIE_DATE_FORMATTER.format(expiresAt));
123+
appendAttribute(header, COOKIE_MAX_AGE_ATTR, String.valueOf(maxAge));
124+
}
125+
126+
String domain = cookie.getDomain();
127+
if (domain != null && !domain.isEmpty()) {
128+
validateDomain(domain);
129+
appendAttribute(header, COOKIE_DOMAIN_ATTR, domain);
130+
}
131+
132+
String path = cookie.getPath();
133+
if (path != null && !path.isEmpty()) {
134+
validatePath(path);
135+
appendAttribute(header, COOKIE_PATH_ATTR, path);
136+
}
137+
138+
if (cookie.getSecure()) {
139+
appendAttributeWithoutValue(header, COOKIE_SECURE_ATTR);
140+
}
141+
142+
if (cookie.isHttpOnly()) {
143+
appendAttributeWithoutValue(header, COOKIE_HTTP_ONLY_ATTR);
144+
}
145+
146+
String sameSite = cookie.getAttribute(COOKIE_SAME_SITE_ATTR);
147+
if (sameSite != null) {
148+
appendAttribute(header, COOKIE_SAME_SITE_ATTR, sameSite);
149+
}
150+
151+
String partitioned = cookie.getAttribute(COOKIE_PARTITIONED_ATTR);
152+
if (EMPTY_STRING.equals(partitioned)) {
153+
appendAttributeWithoutValue(header, COOKIE_PARTITIONED_ATTR);
154+
}
155+
156+
addAdditionalAttributes(cookie, header);
157+
158+
return header.toString();
159+
}
160+
161+
private void appendAttribute(StringBuilder header, String name, String value) {
162+
header.append("; ").append(name);
163+
if (!EMPTY_STRING.equals(value)) {
164+
header.append('=').append(value);
165+
}
166+
}
167+
168+
private void appendAttributeWithoutValue(StringBuilder header, String name) {
169+
header.append("; ").append(name);
170+
}
171+
172+
private void addAdditionalAttributes(Cookie cookie, StringBuilder header) {
173+
for (Map.Entry<String, String> entry : cookie.getAttributes().entrySet()) {
174+
switch (entry.getKey()) {
175+
case COOKIE_COMMENT_ATTR:
176+
case COOKIE_DOMAIN_ATTR:
177+
case COOKIE_MAX_AGE_ATTR:
178+
case COOKIE_PATH_ATTR:
179+
case COOKIE_SECURE_ATTR:
180+
case COOKIE_HTTP_ONLY_ATTR:
181+
case COOKIE_SAME_SITE_ATTR:
182+
case COOKIE_PARTITIONED_ATTR:
183+
// Already handled attributes are ignored
184+
break;
185+
default:
186+
validateAttribute(entry.getKey(), entry.getValue());
187+
appendAttribute(header, entry.getKey(), entry.getValue());
188+
break;
189+
}
190+
}
191+
}
192+
193+
private void validateCookieValue(String value) {
194+
if (!isValidCookieValue(value)) {
195+
throw new IllegalArgumentException("Invalid cookie value: " + value);
196+
}
197+
}
198+
199+
private void validateDomain(String domain) {
200+
if (!isValidDomain(domain)) {
201+
throw new IllegalArgumentException("Invalid cookie domain: " + domain);
202+
}
203+
}
204+
205+
private void validatePath(String path) {
206+
for (char ch : path.toCharArray()) {
207+
if (ch < 0x20 || ch > 0x7E || ch == ';') {
208+
throw new IllegalArgumentException("Invalid cookie path: " + path);
209+
}
210+
}
211+
}
212+
213+
private void validateAttribute(String name, String value) {
214+
if (!isToken(name)) {
215+
throw new IllegalArgumentException("Invalid cookie attribute name: " + name);
216+
}
217+
218+
for (char ch : value.toCharArray()) {
219+
if (ch < 0x20 || ch > 0x7E || ch == ';') {
220+
throw new IllegalArgumentException("Invalid cookie attribute value: " + ch);
221+
}
222+
}
223+
}
224+
225+
private boolean isValidCookieValue(String value) {
226+
int start = 0;
227+
int end = value.length();
228+
boolean quoted = end > 1 && value.charAt(0) == '"' && value.charAt(end - 1) == '"';
229+
230+
char[] chars = value.toCharArray();
231+
for (int i = start; i < end; i++) {
232+
if (quoted && (i == start || i == end - 1)) {
233+
continue;
234+
}
235+
char c = chars[i];
236+
if (!isValidCookieChar(c)) return false;
237+
}
238+
return true;
239+
}
240+
241+
private boolean isValidDomain(String domain) {
242+
if (domain.isEmpty()) {
243+
return false;
244+
}
245+
int prev = -1;
246+
for (char c : domain.toCharArray()) {
247+
if (!domainValid.get(c) || isInvalidLabelStartOrEnd(prev, c)) {
248+
return false;
249+
}
250+
prev = c;
251+
}
252+
return prev != '.' && prev != '-';
253+
}
254+
255+
private boolean isInvalidLabelStartOrEnd(int prev, char current) {
256+
return (prev == '.' || prev == -1) && (current == '.' || current == '-') ||
257+
(prev == '-' && current == '.');
258+
}
259+
260+
private boolean isToken(String s) {
261+
if (s.isEmpty()) return false;
262+
for (char c : s.toCharArray()) {
263+
if (!tokenValid.get(c)) {
264+
return false;
265+
}
266+
}
267+
return true;
268+
}
269+
270+
private boolean isValidCookieChar(char c) {
271+
return !(c < 0x21 || c > 0x7E || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c);
272+
}
273+
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpApiV2ProxyHttpServletRequest.java

+2-15
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.time.ZonedDateTime;
3838
import java.time.format.DateTimeParseException;
3939
import java.util.*;
40-
import java.util.stream.Collectors;
4140
import java.util.stream.Stream;
4241

4342
public class AwsHttpApiV2ProxyHttpServletRequest extends AwsHttpServletRequest {
@@ -81,26 +80,14 @@ public Cookie[] getCookies() {
8180
if (headers == null || !headers.containsKey(HttpHeaders.COOKIE)) {
8281
rhc = new Cookie[0];
8382
} else {
84-
rhc = parseCookieHeaderValue(headers.getFirst(HttpHeaders.COOKIE));
83+
rhc = getCookieProcessor().parseCookieHeader(headers.getFirst(HttpHeaders.COOKIE));
8584
}
8685

8786
Cookie[] rc;
8887
if (request.getCookies() == null) {
8988
rc = new Cookie[0];
9089
} else {
91-
rc = request.getCookies().stream()
92-
.map(c -> {
93-
int i = c.indexOf('=');
94-
if (i == -1) {
95-
return null;
96-
} else {
97-
String k = SecurityUtils.crlf(c.substring(0, i)).trim();
98-
String v = SecurityUtils.crlf(c.substring(i+1));
99-
return new Cookie(k, v);
100-
}
101-
})
102-
.filter(c -> c != null)
103-
.toArray(Cookie[]::new);
90+
rc = getCookieProcessor().parseCookieHeader(String.join("; ", request.getCookies()));
10491
}
10592

10693
return Stream.concat(Arrays.stream(rhc), Arrays.stream(rc)).toArray(Cookie[]::new);

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequest.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public abstract class AwsHttpServletRequest implements HttpServletRequest {
9090
private String queryString;
9191
private Map<String, List<Part>> multipartFormParameters;
9292
private Map<String, List<String>> urlEncodedFormParameters;
93+
private CookieProcessor cookieProcessor;
9394

9495
protected AwsHttpServletResponse response;
9596
protected AwsLambdaServletContainerHandler containerHandler;
@@ -295,12 +296,7 @@ public void setServletContext(ServletContext context) {
295296
* @return An array of Cookie objects from the header
296297
*/
297298
protected Cookie[] parseCookieHeaderValue(String headerValue) {
298-
List<HeaderValue> parsedHeaders = this.parseHeaderValue(headerValue, ";", ",");
299-
300-
return parsedHeaders.stream()
301-
.filter(e -> e.getKey() != null)
302-
.map(e -> new Cookie(SecurityUtils.crlf(e.getKey()), SecurityUtils.crlf(e.getValue())))
303-
.toArray(Cookie[]::new);
299+
return getCookieProcessor().parseCookieHeader(headerValue);
304300
}
305301

306302

@@ -512,6 +508,13 @@ protected Map<String, List<String>> getFormUrlEncodedParametersMap() {
512508
return urlEncodedFormParameters;
513509
}
514510

511+
protected CookieProcessor getCookieProcessor(){
512+
if (cookieProcessor == null) {
513+
cookieProcessor = new AwsCookieProcessor();
514+
}
515+
return cookieProcessor;
516+
}
517+
515518
@Override
516519
public Collection<Part> getParts()
517520
throws IOException, ServletException {

0 commit comments

Comments
 (0)