썬에서 제공하는 모든 JDK/JRE 릴리스에서 사용 가능한 UTF-8 charset 구현은 non-shortest-form UTF-8 바이트 시퀀스를 거부하도록 최근에 업데이트되었습니다. 기존의 구현이 보안 공격에 이용될 소지가 있기 때문입니다. 이 업데이트 이후 필자는 이 "non-shortest-form" 문제가 무엇이며 어떤 영향을 줄 수 있는가에 대한 많은 질문을 받았습니다. 그 중 몇 가지 질문에 대해 여기서 답변하겠습니다.
일반적으로 첫 번째 질문은 "non-shortest-form 문제란 무엇인가?"입니다.
공식적인 자세한 답변은 Unicode Corrigendum #1: UTF-8 Shortest Form에서 확인할 수 있습니다. 요컨대 문제는 많은 사람들의 생각과 달리 유니코드 문자가 "UTF-8 인코딩"에서 둘 이상의 방법(형태)으로 표현될 수 있다는 것입니다. UTF-8 인코딩의 표현 방식에 대해서는 다음과 같은 비트 패턴을 사용하여 간단하게 설명할 수 있습니다.
| # | 비트 | 비트 패턴 | |||
| 1 | 7 | 0xxxxxxx | |||
| 2 | 11 | 110xxxxx | 10xxxxxx | ||
| 3 | 16 | 1110xxxx | 10xxxxxx | 10xxxxxx | |
| 4 | 21 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
이 패턴은 유사한 패턴이지만 최신 UTF-8 정의를 기준으로 하면 사실상 틀린 것입니다. 이 패턴에는 둘 이상의 형태가 하나의 유니코드 문자를 나타낼 수 있다는 허점이 있습니다.
예를 들어, u+0000부터 u+007f까지의 ASCII 문자에서 UTF-8 인코딩 형태는 해당 문자 모두에 대해 투명성을 유지하므로 0x00..0x7f(1바이트 형태)의 해당 ASCII 코드 값이 UTF-8에서 유지됩니다. 그러나 앞의 패턴에 따르면 해당 문자는 [c0, 01]..[c1, bf]와 같은 2바이트 형태, 즉 "non-shortest-form"으로도 나타낼 수 있습니다.
다음 코드는 이 ASCII 문자의 non-shortest-2-bytes-form을 모두 보여 줍니다("기존" 버전의 JDK 및 JRE(Java Runtime Environment)에 대해 코드를 실행할 경우).
byte[] bb = new byte[2];
for (int b1 = 0xc0; b1 < 0xc2; b1++) {
for (int b2 = 0x80; b2 < 0xc0; b2++) {
bb[0] = (byte)b1;
bb[1] = (byte)b2;
String cstr = new String(bb, "UTF8");
char c = cstr.toCharArray()[0];
System.out.printf("[%02x, %02x] -> U+%04x [%s]%n",
b1, b2, c & 0xffff, (c>=0x20)?cstr:"ctrl");
}
}
다음과 같이 출력될 것입니다.
...
[c0, a0] -> U+0020 [ ]
[c0, a1] -> U+0021 [!]
...
[c0, b6] -> U+0036 [6]
[c0, b7] -> U+0037 [7]
[c0, b8] -> U+0038 [8]
[c0, b9] -> U+0039 [9]
...
[c1, 80] -> U+0040 [@]
[c1, 81] -> U+0041 [A]
[c1, 82] -> U+0042 [B]
[c1, 83] -> U+0043 [C]
[c1, 84] -> U+0044 [D]
...
따라서 "ABC"와 같은 문자열은 다음과 같은 2개의 UTF-8 시퀀스 형태를 갖습니다.
"0x41 0x42 0x43" and "0xc1 0x81 0xc1 0x82 0xc1 0x83"
Unicode Corrigendum #1: UTF-8 Shortest Form에서는 "각 UTF의 정의는 해당 UTF에서 잘못된 코드 단위 시퀀스를 지정한다. 예를 들어, UTF-8(D36)의 정의에서는 [C0, AF]와 같은 코드 단위 시퀀스를 잘못된 것으로 지정한다."라고 명시적으로 지정합니다.
기존 구현에서는 이러한 non-shortest-form을 (인코딩 시 생성하지는 않으나) 허용합니다. 이제 새로운 UTF_8 charset에서는 모든 BMP 문자에 대해 non-shortest-form 바이트 시퀀스를 거부합니다. 아래에 나열된 "유효한 바이트 시퀀스"만 허용됩니다.
/* Legal UTF-8 Byte Sequences
*
* # Code Points Bits Bit/Byte pattern
* 1 7 0xxxxxxx
* U+0000..U+007F 00..7F
* 2 11 110xxxxx 10xxxxxx
* U+0080..U+07FF C2..DF 80..BF
* 3 16 1110xxxx 10xxxxxx 10xxxxxx
* U+0800..U+0FFF E0 A0..BF 80..BF
* U+1000..U+FFFF E1..EF 80..BF 80..BF
* 4 21 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
* U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
* U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
* U+100000..U10FFFF F4 80..8F 80..BF 80..BF
*/
다음 질문은 "기존 버전의 JDK/JRE를 계속 사용할 경우 어떤 문제가 생길 수 있는가?"입니다.
일단 필자는 보안 전문가가 아니므로 :-) 제 생각은 중요하지 않습니다. 대신 우리의 보안 전문가에게 문의했습니다. 그들이 내린 결론은 "Java SE 자체의 보안 취약성은 아니지만, UTF-8 시퀀스의 이러한 non-shortest-form을 거부하기 위해 UTF-8 charset을 사용하는 소프트웨어가 시스템에서 실행 중인 경우 이를 공격하는 데 이용될 수 있다"는 것입니다.
"공격하는 데 이용될 수 있다"의 의미를 이해하기 위해 다음과 같은 간단한 시나리오를 들어 보겠습니다.
- 어떤 Java 애플리케이션에서 수신 UTF-8 입력 스트림을 필터링하여 "ABC"와 같은 특정 키워드를 거부하려고 합니다.
- 예를 들어, 입력 UTF-8 바이트 시퀀스를 자바 문자 표현 방식으로 디코딩한 다음 자바 "char" 레벨에서 키워드 문자열 "ABC"를 필터링하는 대신
애플리케이션에서 원시 UTF-8 바이트 시퀀스인 "0x41 0x42 0x43"만 UTF-8 바이트 입력 스트림에 대해 직접 필터링하고, 대상 키워드의 기타 non-shortest-form이 있는 경우 자바 UTF-8 charset를 사용하여 거부하도록 선택할 수 있습니다.String utfStr = new String(bytes, "UTF-8"); if ("ABC".equals(strUTF)) { ... } - 따라서 기본 JDK/JRE 런타임이 기존 버전인 경우 non-shortest-form 입력 "0xc1 0x81 0xc1 0x82 0xc1 0x83"이 필터를 통과하여 보안 취약성을 유발할 수 있습니다.
따라서 이러한 잠재적 위험을 예방하기 위해 최신 JDK/JRE 릴리스로 업데이트하는 것이 좋습니다.
업데이트를 통해 누릴 수 있는 또 다른 큰 이점은 성능입니다.
UTF-8 charset 구현은 오랫동안 업데이트 또는 수정되지 않았습니다. UTF-8 인코딩은 XML의 기본 인코딩으로 널리 사용되는 중이며, UTF-8을 페이지 인코딩으로 사용하는 웹 사이트가 늘고 있습니다. 이 점을 감안하여 "작동되는 한 변경하지 않는다"는 방어적 자세를 지난 수 년 동안 취해 왔습니다.
따라서 Martin과 필자는 이번 기회에 속도도 높이기로 했습니다. 다음 데이터는 벤치마크 실행 데이터 중 하나로서 -server vm에서 새 구현과 기존 구현의 디코딩/인코딩 작업을 비교한 것입니다. (이는 공식적인 벤치마크가 아닙니다. 오로지 성능 향상에 대한 대략적인 이해를 돕기 위해 마련된 것입니다.)
새로운 구현에서는 특히 싱글 바이트(ASCII)를 디코딩 또는 인코딩할 때 속도가 크게 향상됩니다. 새로운 디코딩 및 인코딩은 -client vm에서도 빨라지지만 -server vm에서만큼 큰 격차를 보이지는 않습니다. 필자는 최상의 결과를 보여드리고 싶었습니다. :-)
Method Millis Millis(OLD)
Decoding 1b UTF-8 : 1786 12689
Decoding 2b UTF-8 : 21061 30769
Decoding 3b UTF-8 : 23412 44256
Decoding 4b UTF-8 : 30732 35909
Decoding 1b (direct)UTF-8 : 16015 22352
Decoding 2b (direct)UTF-8 : 63813 82686
Decoding 3b (direct)UTF-8 : 89999 111579
Decoding 4b (direct)UTF-8 : 73126 60366
Encoding 1b UTF-8 : 2528 12713
Encoding 2b UTF-8 : 14372 33246
Encoding 3b UTF-8 : 25734 26000
Encoding 4b UTF-8 : 23293 31629
Encoding 1b (direct)UTF-8 : 18776 19883
Encoding 2b (direct)UTF-8 : 50309 59327
Encoding 3b (direct)UTF-8 : 77006 74286
Encoding 4b (direct)UTF-8 : 61626 66517
새로운 UTF-8 charset 구현은 JDK7, Open JDK 6, JDK 6 업데이트 11 이상, JDK5.0u17 및 1.4.2_19에서 통합되었습니다.
어떠한 변화가 있는지 궁금하시다면 OpenJDK7에 대한 새 UTF_8.java의 웹 리뷰를 참조하십시오.
Xueming Shen은 썬마이크로시스템즈 소속의 엔지니어로서 Java 코어 기술 그룹에서 일하고 있습니다.
이 글의 영문 원본은
Overhauling the Java UTF-8 charset
에서 보실 수 있습니다.
"Java SE" 카테고리의 다른 글
- JSSE 이용한 안전한 커뮤니케이션 (댓글 2개 / 트랙백 0개) 2004/08/31
- 3D 화면(scene)에 빛 효과 주기 (댓글 1개 / 트랙백 0개) 2004/07/30
- Java SE & Java SE for Business 지원 로드맵 (댓글 0개 / 트랙백 0개) 2009/09/11
- 클래스에서 enhanced For-Loop 사용 (댓글 0개 / 트랙백 1개) 2007/10/09
- VARIABLE CONTENT로 메세지 포맷하기 (댓글 7개 / 트랙백 2개) 2003/08/19
- CONTENTPANE 작업의 변화 (댓글 1개 / 트랙백 0개) 2004/11/11
- 새로운 포매터로 출력물 포맷하기 (댓글 1개 / 트랙백 0개) 2004/10/27
- 스윙 유저 인터페이스에서 컴포넌트의 방향성 (댓글 2개 / 트랙백 0개) 2003/09/26
- 쓰레드의 상태정보를 저장할 때 사용되는 THREADLOCAL 변수들 (댓글 4개 / 트랙백 0개) 2003/12/12
- 2개의 스트링이 같은 경우는? (댓글 2개 / 트랙백 0개) 2004/05/27
댓글을 달아 주세요