자바 UTF-8 charset 집중 탐구

Java SE 2009/04/06 19:51 Posted by Sun

썬에서 제공하는 모든 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을 사용하는 소프트웨어가 시스템에서 실행 중인 경우 이를 공격하는 데 이용될 수 있다"는 것입니다.

"공격하는 데 이용될 수 있다"의 의미를 이해하기 위해 다음과 같은 간단한 시나리오를 들어 보겠습니다.

  1. 어떤 Java 애플리케이션에서 수신 UTF-8 입력 스트림을 필터링하여 "ABC"와 같은 특정 키워드를 거부하려고 합니다.
  2. 예를 들어, 입력 UTF-8 바이트 시퀀스를 자바 문자 표현 방식으로 디코딩한 다음 자바 "char" 레벨에서 키워드 문자열 "ABC"를 필터링하는 대신
           
    String utfStr = new String(bytes, "UTF-8");
           if ("ABC".equals(strUTF)) { ... }
    애플리케이션에서 원시 UTF-8 바이트 시퀀스인 "0x41 0x42 0x43"만 UTF-8 바이트 입력 스트림에 대해 직접 필터링하고, 대상 키워드의 기타 non-shortest-form이 있는 경우 자바 UTF-8 charset를 사용하여 거부하도록 선택할 수 있습니다.
  3. 따라서 기본 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" 카테고리의 다른 글

2009/04/06 19:51 2009/04/06 19:51

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/771

  1. 하얀말의 생각

    Tracked from ryudaewan's me2DAY  삭제

    자바 UTF-8 charset 집중 탐구. 문자세트(charset)은 정팔 어려버요 T.T

    2009/04/27 23:01

댓글을 달아 주세요

[로그인][오픈아이디란?]

◀ Prev 1  ... 101 102 103 104 105 106 107 108 109  ... 806  Next ▶