RFC 3629
고친 사람 github-actions[bot]
RFC 3629 는 인터넷에서 글자를 바이트로 적는 방식인 UTF-8 을 표준으로 못박는 문서입니다. UTF-8 은 Unicode Transformation Format 을 줄인 이름입니다. 이 문서는 글자마다 바이트를 몇 개 쓰고 그 바이트를 어떻게 채우는지 표 하나로 정합니다. 그 표에 안 맞는 바이트열은 글자로 읽지 말라고 요구합니다.
쉽고 빠른 이해
RFC 3629 는 UTF-8 이라는 글자 적는 방식을 인터넷 표준으로 굳힌 문서입니다. 이 문서를 따르면 한 은 세 바이트 ED 95 9C 로 적습니다. 이렇게 적는 방법은 하나뿐입니다.
이전 판은 한 글자를 여섯 바이트까지 쓰게 했습니다. 같은 글자를 더 긴 바이트로 적는 것도 막지 않았습니다. 그 틈으로 바이트 검사를 피해 가는 공격이 실제로 나왔습니다. 이 문서는 범위를 좁히고 모양을 하나로 묶었습니다.
- 글자 번호의 크기에 따라 한 바이트부터 네 바이트까지 씁니다
- 첫 바이트의 앞쪽 비트가 몇 바이트짜리인지 알려 줍니다. 뒤따르는 바이트는
10으로 시작합니다 - 이 모양에서 벗어난 바이트열은 글자로 읽지 않습니다
대가도 있습니다. 이전 판이 허용하던 다섯·여섯 바이트 글자는 이제 못 적습니다. 읽는 프로그램은 바이트마다 모양을 검사해야 합니다.
프로토콜이 「텍스트는 UTF-8 로 적는다」고 할 때 따르는 규칙이 이 문서입니다. 바이트를 글자로 읽는 코드나 입력 검사를 짤 때 펴 봅니다. 브라우저가 바이트를 글자로 바꾸는 절차는 웹 표준이 따로 정합니다.
상세
이 절은 RFC(Request for Comments) 3629 가 정하는 바이트 표와, 그 표에서 벗어난 바이트열을 다루는 규칙을 봅니다.
문서의 정체
RFC 는 인터넷 기술을 번호 붙은 문서로 내는 문서 묶음입니다. IETF(Internet Engineering Task Force, 인터넷 표준을 만드는 단체)가 이 묶음을 냅니다. 3629번 문서의 제목은 「UTF-8, a transformation format of ISO 10646」입니다.
나온 때는 2003년 11월입니다. 쓴 사람은 François Yergeau 한 명입니다. 갈래는 표준으로 올라가는 Standards Track 입니다. 지금은 인터넷 표준 단계에 올라 있습니다.
인터넷 표준까지 올라간 문서에는 RFC 번호와 별도로 STD 번호(Standard number, 인터넷 표준 번호)가 붙습니다. RFC 3629 의 STD 번호는 63 입니다. 같은 내용을 새 RFC 가 대신하더라도 STD 번호는 바뀌지 않으므로, 다른 문서가 「UTF-8 표준」을 오래 가리킬 때 쓰기 좋습니다.
이 문서는 1998년의 RFC 2279 를 폐기하고 대신합니다. RFC 2279 를 인용한 옛 문서를 읽을 때도 지금 따를 규칙은 RFC 3629 에서 찾습니다.
요구의 세기는 대문자 낱말로 가립니다. 이 낱말들의 뜻은 RFC 2119 가 정합니다.
| 낱말 | 뜻 |
|---|---|
| MUST | 어기면 표준을 안 지킨 것이다 |
| SHOULD | 이유가 있을 때만 어길 수 있다 |
| MAY | 해도 되고 안 해도 된다 |
무엇을 정하는 문서인가
컴퓨터는 글자를 번호로 바꿔 다룹니다. 유니코드는 세계의 글자마다 번호를 하나씩 매긴 표입니다.
이 번호를 코드 포인트라고 부릅니다. 코드 포인트는 U+D55C 처럼 16진수로 적습니다. U+D55C 는 한 의 번호입니다.
번호만 정해서는 파일에 쓸 수 없습니다. 번호를 바이트 몇 개로 어떻게 적을지가 따로 필요합니다. 이 규칙을 문자 인코딩이라 합니다. UTF-8(Unicode Transformation Format, 8비트 단위로 적는 형식)은 그런 규칙의 하나입니다.
제목의 ISO 10646 은 ISO/IEC 10646 이라는 국제 표준입니다. 유니코드와 같은 글자 표를 담습니다. 두 표는 번호를 맞춰 함께 고쳐 나갑니다. 제목은 UTF-8 이 그 글자 표를 바이트로 옮기는 방식이라는 뜻입니다.
이 표준을 낸 곳은 ISO(International Organization for Standardization, 국제표준화기구)와 IEC(International Electrotechnical Commission, 국제전기기술위원회)입니다. 이름의 ISO/IEC 가 이 두 단체입니다.
UTF-8 의 정본 정의는 유니코드 표준이 갖습니다. RFC 3629 는 그 정의를 인터넷 쪽으로 옮겨 옵니다. 그리고 프로토콜과 구현이 지킬 MUST 와 SHOULD 를 붙입니다.
바이트 수를 정하는 표
문서의 중심은 표 하나입니다. 코드 포인트가 어느 범위에 드느냐에 따라 바이트 수와 각 바이트의 앞쪽 비트가 정해집니다. 아래 x 칸에는 코드 포인트의 비트가 들어갑니다.
| 코드 포인트 범위 | 바이트 수 | 바이트 모양 |
|---|---|---|
U+0000 ~ U+007F |
1 | 0xxxxxxx |
U+0080 ~ U+07FF |
2 | 110xxxxx 10xxxxxx |
U+0800 ~ U+FFFF |
3 | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 ~ U+10FFFF |
4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
첫 바이트 앞쪽의 1 개수가 그 글자의 바이트 수입니다. 뒤따르는 바이트는 모두 10 으로 시작합니다. 그래서 바이트열 중간 아무 데서 읽기 시작해도 10 으로 시작하지 않는 바이트를 찾으면 글자의 시작을 알 수 있습니다.
표의 범위는 서로 겹치지 않습니다. 한 코드 포인트가 들어갈 줄은 한 줄로 정해집니다. 따라서 한 글자를 UTF-8 로 적는 올바른 방법도 하나입니다.
한(U+D55C)을 표에 넣어 보겠습니다. U+D55C 는 셋째 줄 범위에 들어가므로 세 바이트를 씁니다. 번호의 비트 16개를 앞에서부터 4·6·6 개로 잘라 각 바이트의 빈칸을 채웁니다.
U+D55C → 1101 010101 011100
1110 + 1101 → 11101101 // ED
10 + 010101 → 10010101 // 95
10 + 011100 → 10011100 // 9C
맨 윗줄은 번호를 비트로 풀어 넷·여섯·여섯으로 끊은 것입니다. 아래 세 줄은 그 조각을 표의 앞쪽 비트 뒤에 붙인 결과입니다. 이렇게 한 은 ED 95 9C 가 됩니다. 「한국어」 세 글자는 ED 95 9C EA B5 AD EC 96 B4 입니다.
표의 넷째 줄이 U+10FFFF 에서 끝나는 까닭은 UTF-16 에 있습니다. UTF-16 은 UTF-8 과 다른, 글자를 2바이트 단위로 적는 인코딩입니다. UTF-16 이 적을 수 있는 가장 큰 번호가 U+10FFFF 입니다. UTF-8 의 범위를 여기에 맞춰서 두 인코딩이 같은 글자 집합을 담습니다.
영어 글자와 겹치는 값
첫째 줄을 보면 U+0000 부터 U+007F 까지는 한 바이트에 번호를 손대지 않고 담습니다. 이 범위가 영어 글자·숫자·기호를 담은 ASCII(American Standard Code for Information Interchange)와 같습니다. 영어만 든 ASCII 파일은 고치지 않아도 올바른 UTF-8 입니다.
반대로 00 ~ 7F 값은 여러 바이트짜리 글자 안에는 나오지 않습니다. 00 ~ 7F 는 맨 앞 비트가 0 인 값입니다. 여러 바이트짜리 글자의 바이트는 11 이나 10 으로 시작하므로 맨 앞 비트가 늘 1 입니다. 그래서 / 나 줄바꿈 같은 ASCII 값으로 문자열을 자르는 옛 프로그램도 한글이 섞인 UTF-8 을 깨뜨리지 않습니다.
올바른 바이트열의 문법
표만 보면 막히지 않는 바이트열이 있습니다. 대표가 과잉 길이입니다. 더 적은 바이트로 충분한 글자를 더 많은 바이트에 억지로 담는 것을 말합니다. 예를 들어 U+0000 을 C0 80 두 바이트로 적으면 비트 모양만은 둘째 줄에 맞습니다.
또 하나는 UTF-16 에서 온 문제입니다. UTF-16 은 U+FFFF 보다 큰 번호를 2바이트 조각 둘로 나눠 적습니다. 이 조각에 쓰려고 비워 둔 번호 U+D800 ~ U+DFFF 를 서로게이트라고 부릅니다. 서로게이트는 글자 번호가 아닌데도 셋째 줄 모양으로는 적을 수 있습니다.
RFC 3629 는 이런 바이트열까지 걸러 내도록 올바른 바이트열의 문법을 따로 둡니다. 표기는 ABNF(Augmented Backus-Naur Form)입니다. ABNF 는 인터넷 표준 문서들이 「어떤 바이트가 어떤 순서로 와야 하나」를 적을 때 쓰는 문법 표기법입니다.
핵심은 첫 바이트에 따라 다음 바이트로 올 수 있는 값을 좁힌 것입니다. 먼저 두 바이트 글자의 첫 바이트는 C2 ~ DF 만 씁니다. C0 · C1 로 시작하면 담기는 번호가 U+007F 이하입니다. 한 바이트로 충분한 글자이니 과잉 길이입니다.
세 바이트와 네 바이트 글자는 첫 바이트를 보고 둘째 바이트의 범위를 좁힙니다.
| 첫 바이트 | 둘째 바이트로 허용 | 막는 것 |
|---|---|---|
E0 |
A0 ~ BF |
세 바이트 과잉 길이 |
ED |
80 ~ 9F |
서로게이트 범위 |
F0 |
90 ~ BF |
네 바이트 과잉 길이 |
F4 |
80 ~ 8F |
U+10FFFF 를 넘는 번호 |
E0 뒤에 80 ~ 9F 가 오면 번호가 U+0800 보다 작아집니다. 두 바이트로 충분한 글자를 세 바이트에 담은 것입니다. F0 뒤에 80 ~ 8F 가 오면 번호가 U+10000 보다 작아 세 바이트로 충분합니다. ED 뒤에 A0 이상이 오면 서로게이트 범위에, F4 뒤에 90 이상이 오면 U+10FFFF 너머에 닿습니다.
나머지 첫 바이트(E1 ~ EC, EE ~ EF, F1 ~ F3)는 둘째 바이트로 80 ~ BF 를 모두 받습니다. 이 문법은 구현을 돕기 위한 것입니다. 이 ABNF 문법과 유니코드 표준의 정의가 어긋나면 유니코드 쪽을 따릅니다.
이 문법을 따르면 바이트 값 C0 · C1 과 F5 ~ FF 는 올바른 UTF-8 에 한 번도 나오지 않습니다. C0 · C1 은 위에서 본 대로 과잉 길이만 만듭니다. F5 ~ FF 로 시작하면 번호가 U+10FFFF 를 넘거나 표에 없는 다섯 바이트 이상의 모양이 됩니다. 이 값이 보이면 그 바이트열은 UTF-8 이 아니거나 망가진 것입니다.
표에 안 맞는 바이트열
UTF-8 바이트를 글자로 되돌리는 일을 디코딩이라 합니다. 그 일을 하는 프로그램이 디코더입니다. 디코더는 올바르지 않은 바이트열을 글자로 읽지 않도록 막아야 합니다(MUST). 이전 판에서는 주의 문구였던 것을 이 판이 반드시 지킬 규정으로 올렸습니다.
규정을 세게 만든 까닭은 보안입니다. 입력을 검사하는 코드와 그 입력을 해석하는 코드가 서로 다르게 읽으면 검사가 뚫립니다. 대표적인 예가 둘입니다.
첫째는 널 문자입니다. 검사기가 한 바이트 00 은 막는데 C0 80 은 통과시킵니다. 뒤에 있는 순진한 디코더가 C0 80 을 U+0000 으로 읽으면 막으려던 널 문자가 들어옵니다.
둘째는 경로 순회입니다. 파일 경로에서 /../ 는 한 단계 위 폴더로 올라가라는 뜻입니다. 웹 서버는 이 바이트열을 막습니다. 바이트로는 / 가 2F, . 가 2E 라서 /../ 는 2F 2E 2E 2F 입니다.
공격자는 두 점 가운데 첫 점을 C0 AE 로 바꿔 보냅니다. C0 AE 는 점(U+002E)을 두 바이트에 억지로 담은 과잉 길이입니다.
sequenceDiagram
participant 공격자
participant 검사기
participant 디코더
공격자->>검사기: 2F C0 AE 2E 2F
Note over 검사기: 바이트에 2F 2E 2E 2F 가 없어 통과시킨다
검사기->>디코더: 그대로 넘긴다
Note over 디코더: C0 AE 를 점으로 읽어 /../ 가 된다
검사기는 바이트를 봅니다. 디코더는 글자를 봅니다. 둘이 같은 바이트열을 다르게 읽은 틈으로 위 폴더에 닿습니다.
쓰는 쪽의 버퍼 문제
글자를 UTF-8 바이트로 적는 쪽, 곧 인코더에도 위험이 있습니다. ISO/IEC 10646 의 옛 정의는 코드 포인트를 U+7FFFFFFF 까지 허용했습니다. 그 번호는 여섯 바이트가 됩니다.
인코더가 번호를 U+10FFFF 까지로 제한하지 않으면 글자 하나에 다섯·여섯 바이트를 씁니다. 버퍼를 네 바이트 기준으로 잡아 둔 프로그램은 거기서 버퍼 오버플로를 냅니다. RFC 3629 는 범위를 U+10FFFF 로 못박아 이 경우를 없앴습니다.
같아 보이는 글자의 다른 바이트
UTF-8 은 한 글자를 한 가지 바이트로만 적습니다. 그래도 사람 눈에 같은 글자가 서로 다른 바이트로 들어올 수 있습니다. 글자 자체를 여러 방식으로 조합할 수 있기 때문입니다.
é 는 한 글자 U+00E9 로도, e(U+0065) 뒤에 악센트 부호(U+0301)를 붙인 두 글자로도 적힙니다. 둘은 화면에 똑같이 보이지만 바이트를 비교하면 다릅니다.
이 차이는 아이디를 권한 목록과 대조하거나 문자열을 검색할 때 문제를 만듭니다. RFC 3629 는 이 위험을 막는 방법으로 유니코드 정규화를 가리킵니다. 정규화는 같은 글자의 여러 표기를 하나로 맞추는 처리입니다.
앞머리 표지 BOM
BOM(Byte Order Mark, 바이트 순서 표시)은 텍스트 맨 앞에 붙이는 글자 U+FEFF 입니다. UTF-16 처럼 바이트 순서가 갈리는 인코딩에서 순서를 알리려고 씁니다. UTF-8 로 적으면 EF BB BF 세 바이트가 됩니다.
UTF-8 은 바이트 순서가 갈리지 않아 순서를 알릴 일이 없습니다. 그래서 UTF-8 에서 BOM 은 「이 텍스트는 UTF-8 이다」라는 표지로만 쓰입니다. 문제는 U+FEFF 가 원래 폭 없는 공백이라는 글자이기도 하다는 점입니다. 맨 앞의 U+FEFF 가 표지인지 글자인지 늘 모호합니다.
RFC 3629 는 이 모호함을 줄이는 규칙을 둡니다. 맨 앞이 아닌 데 나온 U+FEFF 는 폭 없는 공백으로 읽어야 합니다. 표지로 읽으면 안 됩니다(MUST · MUST NOT). 맨 앞의 U+FEFF 를 어떻게 다룰지는 프로토콜이 경우에 따라 정하도록 권합니다.
아래 표의 「칸」은 프로토콜에서 텍스트가 실리는 부분을 말합니다. 메일 본문이나 웹 요청의 본문이 그 예입니다.
| 프로토콜의 칸이 이런 경우 | BOM 표지를 |
|---|---|
| 늘 UTF-8 이라고 정해 둔 칸 | 금지하기를 권한다 (SHOULD) |
| 인코딩을 따로 알리는 수단이 있는 칸 | 금지하기를 권한다 (SHOULD) |
| 알릴 수단이 없거나 금지해도 지켜지지 않을 칸 | 금지하지 말기를 권한다 (SHOULD NOT) |
첫 줄은 표지가 알려 줄 것이 없는 경우입니다. 셋째 줄은 표지 말고는 인코딩을 알 길이 없는 경우입니다. 금지한 칸에서 맨 앞에 U+FEFF 가 오면 폭 없는 공백 글자로 읽어야 합니다(MUST).
메일과 HTTP 의 이름표
MIME(Multipurpose Internet Mail Extensions, 다목적 인터넷 메일 확장)은 메일 머리에 본문의 종류와 인코딩을 적는 규칙입니다. HTTP(Hypertext Transfer Protocol, 웹에서 요청과 응답을 주고받는 규약)도 같은 방식을 빌려 씁니다.
인코딩 이름은 charset 매개변수에 적습니다. Content-Type: text/plain; charset=UTF-8 처럼 씁니다. 여기에 넣는 이름 UTF-8 은 RFC 3629 를 근거로 등록돼 있습니다.
이 이름표에는 일부러 판 번호를 붙이지 않았습니다. 유니코드는 판을 올리며 글자를 계속 더합니다. 판 번호가 붙은 이름이면 받는 쪽은 모르는 새 판 이름을 만났을 때 아예 못 읽습니다. 판 없는 이름이면 새 글자가 몇 개 섞여 있어도 나머지는 제대로 읽습니다.
이전 판에서 바뀐 것
RFC 3629 가 RFC 2279 에서 바꾼 점은 아래와 같습니다.
| 바뀐 것 | 내용 |
|---|---|
| 범위 | 코드 포인트를 U+10FFFF 까지로 줄였다. 최대 여섯 바이트가 네 바이트가 됐다 |
| 정본 정의 | UTF-8 의 정의는 유니코드 표준을 따르고, 글자 목록은 ISO/IEC 10646 을 따른다 |
| 올바르지 않은 바이트열 | 주의 문구였던 것을 반드시 지킬 규정(MUST NOT)으로 올렸다 |
| BOM | BOM 절을 새로 두고 프로토콜에 주는 권고를 붙였다 |
| 문법 | 올바른 바이트열의 ABNF 문법을 더했다 |
| 보안 | 보안 절을 늘리고 정규화 문제를 더했다 |
| MIME 이름 | 이전 판이 제안한 UNICODE-1-1-UTF-8 이름을 뺐다 |
이 문서를 펴 보는 때
프로토콜이나 파일 포맷 문서가 「텍스트는 UTF-8 로 적는다」고 할 때 그 UTF-8 이 대개 RFC 3629 입니다. 데이터 포맷 JSON(JavaScript Object Notation)을 정한 RFC 8259 처럼 다른 RFC 가 이 번호를 직접 인용하기도 합니다.
디코더를 직접 짜거나 입력 검사를 설계할 때도 이 문서를 엽니다. 어떤 바이트열을 거부해야 하는지가 위의 문법 표에 다 들어 있습니다.
반대로 웹 브라우저가 바이트를 글자로 바꾸는 절차는 따로 정해져 있습니다. 망가진 바이트를 대체 문자로 바꾸는 방법이 그 예입니다. 이 절차는 웹 브라우저 표준을 내는 WHATWG(Web Hypertext Application Technology Working Group)의 WHATWG Encoding Standard 가 정합니다.
글자 번호와 글자의 성질은 유니코드 표준이 정합니다. RFC 3629 는 그 사이에서 인터넷에 실리는 UTF-8 바이트의 모양을 맡습니다.
관련 항목
RFC 3629 가 정하는 인코딩과 그 형제
UTF-8 · UTF-16 · UTF-32 · CESU-8 · Modified UTF-8 · 문자 인코딩
RFC 3629 가 기대는 글자 표준
유니코드 · ISO/IEC 10646 · 코드 포인트 · 서로게이트 · 유니코드 스칼라 값 · ASCII
RFC 3629 가 대신한 문서와 기대는 RFC
RFC 2279 · RFC 2119 · RFC 2978 · RFC 5234 · ABNF · RFC 8259
RFC 3629 를 펴낸 표준 체계
RFC · IETF · STD 번호 · Standards Track · ISO
RFC 3629 가 막으려는 공격과 오류
과잉 길이 · 경로 순회 · 널 문자 · 버퍼 오버플로 · 유니코드 정규화 · 대체 문자 · 모지바케
RFC 3629 의 이름표를 쓰는 규칙
MIME · Content-Type · charset · BOM · HTTP · JSON
UTF-8 바이트를 다루는 다른 표준
WHATWG Encoding Standard · 인코딩 스니핑 알고리즘 · 디코딩 · 인코딩
다른 이름: RFC3629 · STD 63