유효한 HTTP 쿠키 값 - RFC 6265(HTTP State Management Mechanism)
1. Problem Context
최근 쿠키(cookie) 허용 팝업 기능을 구현했다. 사용자의 쿠키 사용 여부에 대한 동의 여부를 쿠키로 만들고 데이터베이스에 저장하기 위해 스프링 애플리케이션 서버로 전송했다. 여기서 브라우저가 전송한 쿠키를 스프링 애플리케이션에서 찾을 수 없는 문제가 발생했다. 스프링 애플리케이션으로 전송한 쿠키는 다음과 같다.
consent_data=analytics:true,marketing:false,social:true
스프링 애플리케이션에선 HttpServletRequest 객체의 getCookies 메소드로 조회했다. 예를 들면 아래 코드처럼 요청 정보에서 쿠키 리스트를 조회했다.
@GetMapping("/cookies")
public List<String> getCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
return Arrays.stream(cookies)
.map(cookie -> String.join("=", cookie.getName(), cookie.getValue()))
.toList();
}
return Collections.emptyList();
}
2. Cause of the problem
HTTP 요청을 보면 확실히 쿠키는 전송했다. 쿠키 전송은 정상적이지만, 어째서 HttpServletRequest 객체로부터 찾을 수 없을까? 이 문제의 원인을 찾기 위해 적지 않은 시간을 사용했다. HttpServletRequest 객체의 getCookies 메소드을 따라 들어가면 다음과 같은 클래스들을 만난다.
- org.apache.tomcat.util.http.Rfc6265CookieProcessor
- org.apache.tomcat.util.http.parser.Cookie
HTTP 헤더로부터 쿠키 정보를 파싱할 때 두 객체가 협력한다. 최종적으론 Cookie 클래스의 readCookieValueRfc6265 메소드가 호출된다. readCookieValueRfc6265 메소드는 RFC 6265 스펙의 cookie-octet
정의에 맞춰서 유효하지 않은 문자가 포함된 쿠키인 경우엔 null 값을 반환한다.
public class Cookie {
private static final boolean[] isCookieOctet = new boolean[256];
private static ByteBuffer readCookieValueRfc6265(ByteBuffer bb) {
boolean quoted = false;
if (bb.hasRemaining()) {
if (bb.get() == QUOTE_BYTE) {
quoted = true;
} else {
bb.rewind();
}
}
int start = bb.position();
int end = bb.limit();
while (bb.hasRemaining()) {
byte b = bb.get();
if (isCookieOctet[(b & 0xFF)]) {
// NO-OP
} else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == TAB_BYTE) {
end = bb.position() - 1;
bb.position(end);
break;
} else if (quoted && b == QUOTE_BYTE) {
end = bb.position() - 1;
break;
} else {
// return null when invalid charater in cookie
return null;
}
}
return new ByteBuffer(bb.bytes, start, end - start);
}
}
Cookie 객체가 따르는 RFC 6265 명세는 무엇일까? 이 명세는 HTTP Cookie 헤더와 Set-Cookie 헤더 필드를 정의한다. 이 명세를 살펴보면 쿠키 값(cookie value)으로 허용된 cookie-octet
에 대한 정의를 찾을 수 있다.
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
위 내용을 정리해보자. 쿠키 값은 다음 두 가지 형식 중 하나를 따른다.
- 단순히 유효한 문자(cookie-octet)들의 나열
- 또는, 쌍따옴표(“)로 감싼 형태의 유효한 문자들의 나열
아스키 코드 기준으로 다음 문자만 허용한다.
문자 범위 | 아스키 코드 범위 | 예시 문자 | 설명 |
---|---|---|---|
! |
0x21 | ! | 단일 문자 |
# ~ + |
0x23 ~ 0x2B | # $ % & ‘ ( ) * + | 연속 범위 |
- ~ : |
0x2D ~ 0x3A | - . / 0 1 2 3 4 5 6 7 8 9 : | 숫자 포함 |
< ~ [ |
0x3C ~ 0x5B | < = > ? @ A ~ Z [ | 대문자 포함 |
] ~ ~ |
0x5D ~ 0x7E | ] ^ _ ` a ~ z { | 소문자 포함 |
다음과 같은 문자는 허용하지 않는다.
문자 | 아스키 코드 | 이유 |
---|---|---|
(공백) | 0x20 | 공백 문자 허용 안 됨 |
" (따옴표) |
0x22 | 문자열 경계 구분자 |
, (쉼표) |
0x2C | 헤더 다중 값 구분자 |
; (세미콜론) |
0x3B | 쿠키 속성 구분자 (key=value; ) |
\ (백슬래시) |
0x5C | 이스케이프 문자로 혼동 가능 |
DEL | 0x7F | 제어 문자 |
제어 문자 | 0x00 ~ 0x1F | 텍스트로 안전하지 않음 |
3. Solve the problem
문제 해결은 간단하다. 위에서 전송한 쿠키 값에는 RFC 6265 명세에 정의된 유효하지 않은 문자(, 콤마)
가 포함되어 있었던 것이라 해당 문자만 파이프(|)
로 변경했다.
$ curl -b "consent_data=analytics:true|marketing:false|social:true" http://localhost:8080/cookies | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 59 0 59 0 0 1069 0 --:--:-- --:--:-- --:--:-- 1072
[
"consent_data=analytics:true|marketing:false|social:true"
]
댓글남기기