[EXT-009][중급] 같아 보이는 유니코드 문자열 안전하게 비교하기
페이지 정보
작성자 기술팀장 작성일 26-09-07 22:33 조회 15 댓글 0본문
[이번 수업]
화면에는 똑같이 `가`로 보여도 프로그램은 서로 다른 문자 배열로 저장할 수 있습니다. 이번에는 유니코드(세계의 문자를 공통 번호로 표현하는 표준)의 코드 포인트와 정규화(normalization, 같은 의미의 여러 표현을 정해진 형태로 맞추는 과정)를 배우고 안전한 비교 방법을 실습합니다.
[선수지식]
문자열, 길이, 함수 호출을 알고 Python 파일을 실행할 수 있으면 됩니다. 코드 포인트는 유니코드에서 문자 요소에 붙인 `U+AC00` 같은 번호입니다.
[학습목표]
1. 화면이 같은 두 문자열의 바이트나 코드 포인트가 다를 수 있음을 설명한다.
2. NFC와 NFD를 구분하고 비교 전에 알맞은 정규화를 적용한다.
3. 정규화만으로 해결되지 않는 보안 문제를 찾는다.
[핵심개념]
한글 음절 `가`는 완성된 U+AC00 하나로 표현하거나 초성 U+1100과 중성 U+1161을 조합해 표현할 수 있습니다. 두 표현은 표준이 인정하는 정규 동등성(canonical equivalence, 모양과 의미가 같은 관계)을 가지지만, 그대로 비교하면 길이와 코드 포인트가 다릅니다. 검색·중복 검사·파일명 처리에서 눈에 보이지 않는 차이가 오류를 만들 수 있습니다.
유니코드 정규화에는 NFC, NFD, NFKC, NFKD 네 형식이 있습니다. NFD는 정규 동등 문자를 분해하고 NFC는 이를 가능한 한 합성합니다. NFKC와 NFKD는 호환 문자도 바꾸므로 전각 문자나 장식용 표현처럼 사용자가 구분하려던 차이가 사라질 수 있습니다. 일반 텍스트 비교에는 시스템 규칙이 허용할 때 NFC를 일관되게 적용하는 경우가 많고, NFKC는 요구사항을 확인한 뒤 선택합니다.
정규화는 대소문자 처리와 별개입니다. 대소문자를 무시할 필요가 있다면 언어와 제품 규칙을 정한 뒤 `casefold()` 같은 기능을 별도로 검토합니다. 또한 라틴 문자 `a`와 키릴 문자 `а`처럼 비슷하게 보이지만 서로 다른 문자는 NFC로 같아지지 않습니다. 사용자 이름이나 도메인처럼 보안 영향이 있는 값은 Unicode 보안 지침에 따른 혼동 문자 검사와 허용 문자 정책이 추가로 필요합니다.
[따라하기]
아래를 unicode_check.py로 저장합니다. 외부 패키지나 네트워크는 필요하지 않습니다.
```python
import unicodedata
first = "가"
second = "\u1100\u1161"
def points(text):
return " ".join(f"U+{ord(char):04X}" for char in text)
print("그대로 같음:", first == second)
print("코드 포인트 수:", len(first), len(second))
print("첫째 값:", points(first))
print("둘째 값:", points(second))
print("NFC 뒤 같음:", unicodedata.normalize("NFC", first) == unicodedata.normalize("NFC", second))
```
macOS/Linux는 `python3 unicode_check.py`, Windows는 `py unicode_check.py`로 실행합니다. 예상 결과입니다.
```text
그대로 같음: False
코드 포인트 수: 1 2
첫째 값: U+AC00
둘째 값: U+1100 U+1161
NFC 뒤 같음: True
```
Python 결과는 운영체제와 같습니다. 다만 파일시스템이나 외부 프로그램이 파일명을 다른 정규화 형태로 다룰 수 있으므로 화면 표시만 믿지 말고 애플리케이션 경계에서 정책대로 정규화한 값을 비교하세요. 원문 표시가 필요하면 원본과 비교용 정규화 값을 구분해 보관합니다.
[흔한 실수]
UTF-8로 인코딩하면 자동으로 정규화된다고 생각하기 쉽지만 인코딩과 정규화는 다른 과정입니다. 모든 입력에 NFKC를 무조건 적용하거나, 정규화 뒤 데이터베이스의 UNIQUE 검사를 하지 않는 것도 문제입니다. 문자열 길이를 사용자에게 보이는 글자 수와 항상 같다고 보는 실수도 피해야 합니다.
[보안 주의]
본인 소유의 로컬·격리 환경에서만 실습하세요. 정규화는 혼동 문자 공격을 모두 막지 못하므로 계정 식별자는 허용 문자·길이·중복 정책과 인가 검사를 함께 적용합니다. 비밀번호는 가입 때와 로그인 때 같은 명시적 규칙을 사용해야 하며, 기존 값을 임의로 정규화해 바꾸지 않습니다. 로그에는 원문 개인정보를 불필요하게 남기지 마세요.
[직접 해볼 과제]
`first`와 `second`를 NFD로 바꾼 뒤 코드 포인트를 출력해 보세요. 이어 두 값을 리스트에 넣고 NFC 정규화 후 집합 크기가 1인지 확인하고, NFKC를 사용자 입력 전체에 무조건 적용하면 안 되는 이유를 한 문장으로 적으세요.
[확인문제]
1. 화면에 같은 `가`가 그대로 비교하면 다를 수 있는 이유는 무엇입니까?
2. NFC와 NFKC를 선택할 때 확인해야 할 차이는 무엇입니까?
3. 정규화만으로 비슷하게 보이는 다른 문자의 공격을 막을 수 없는 이유는 무엇입니까?
[다음 학습]
다음에는 파일과 API에서 문자 인코딩을 명시하고 잘못된 바이트를 안전하게 처리하는 방법을 배웁니다.
[공식 참고 자료]
- Unicode 정규화 형식 UAX #15: https://www.unicode.org/reports/tr15/
- Unicode 보안 메커니즘 UTS #39: https://www.unicode.org/reports/tr39/
- Python unicodedata 공식 문서: https://docs.python.org/3/library/unicodedata.html
- ECMAScript String.prototype.normalize 명세: https://tc39.es/ecma262/multipage/text-processing.html#sec-string.prototype.normalize
- W3C 문자열 정규화 지침: https://www.w3.org/TR/charmod-norm/
화면에는 똑같이 `가`로 보여도 프로그램은 서로 다른 문자 배열로 저장할 수 있습니다. 이번에는 유니코드(세계의 문자를 공통 번호로 표현하는 표준)의 코드 포인트와 정규화(normalization, 같은 의미의 여러 표현을 정해진 형태로 맞추는 과정)를 배우고 안전한 비교 방법을 실습합니다.
[선수지식]
문자열, 길이, 함수 호출을 알고 Python 파일을 실행할 수 있으면 됩니다. 코드 포인트는 유니코드에서 문자 요소에 붙인 `U+AC00` 같은 번호입니다.
[학습목표]
1. 화면이 같은 두 문자열의 바이트나 코드 포인트가 다를 수 있음을 설명한다.
2. NFC와 NFD를 구분하고 비교 전에 알맞은 정규화를 적용한다.
3. 정규화만으로 해결되지 않는 보안 문제를 찾는다.
[핵심개념]
한글 음절 `가`는 완성된 U+AC00 하나로 표현하거나 초성 U+1100과 중성 U+1161을 조합해 표현할 수 있습니다. 두 표현은 표준이 인정하는 정규 동등성(canonical equivalence, 모양과 의미가 같은 관계)을 가지지만, 그대로 비교하면 길이와 코드 포인트가 다릅니다. 검색·중복 검사·파일명 처리에서 눈에 보이지 않는 차이가 오류를 만들 수 있습니다.
유니코드 정규화에는 NFC, NFD, NFKC, NFKD 네 형식이 있습니다. NFD는 정규 동등 문자를 분해하고 NFC는 이를 가능한 한 합성합니다. NFKC와 NFKD는 호환 문자도 바꾸므로 전각 문자나 장식용 표현처럼 사용자가 구분하려던 차이가 사라질 수 있습니다. 일반 텍스트 비교에는 시스템 규칙이 허용할 때 NFC를 일관되게 적용하는 경우가 많고, NFKC는 요구사항을 확인한 뒤 선택합니다.
정규화는 대소문자 처리와 별개입니다. 대소문자를 무시할 필요가 있다면 언어와 제품 규칙을 정한 뒤 `casefold()` 같은 기능을 별도로 검토합니다. 또한 라틴 문자 `a`와 키릴 문자 `а`처럼 비슷하게 보이지만 서로 다른 문자는 NFC로 같아지지 않습니다. 사용자 이름이나 도메인처럼 보안 영향이 있는 값은 Unicode 보안 지침에 따른 혼동 문자 검사와 허용 문자 정책이 추가로 필요합니다.
[따라하기]
아래를 unicode_check.py로 저장합니다. 외부 패키지나 네트워크는 필요하지 않습니다.
```python
import unicodedata
first = "가"
second = "\u1100\u1161"
def points(text):
return " ".join(f"U+{ord(char):04X}" for char in text)
print("그대로 같음:", first == second)
print("코드 포인트 수:", len(first), len(second))
print("첫째 값:", points(first))
print("둘째 값:", points(second))
print("NFC 뒤 같음:", unicodedata.normalize("NFC", first) == unicodedata.normalize("NFC", second))
```
macOS/Linux는 `python3 unicode_check.py`, Windows는 `py unicode_check.py`로 실행합니다. 예상 결과입니다.
```text
그대로 같음: False
코드 포인트 수: 1 2
첫째 값: U+AC00
둘째 값: U+1100 U+1161
NFC 뒤 같음: True
```
Python 결과는 운영체제와 같습니다. 다만 파일시스템이나 외부 프로그램이 파일명을 다른 정규화 형태로 다룰 수 있으므로 화면 표시만 믿지 말고 애플리케이션 경계에서 정책대로 정규화한 값을 비교하세요. 원문 표시가 필요하면 원본과 비교용 정규화 값을 구분해 보관합니다.
[흔한 실수]
UTF-8로 인코딩하면 자동으로 정규화된다고 생각하기 쉽지만 인코딩과 정규화는 다른 과정입니다. 모든 입력에 NFKC를 무조건 적용하거나, 정규화 뒤 데이터베이스의 UNIQUE 검사를 하지 않는 것도 문제입니다. 문자열 길이를 사용자에게 보이는 글자 수와 항상 같다고 보는 실수도 피해야 합니다.
[보안 주의]
본인 소유의 로컬·격리 환경에서만 실습하세요. 정규화는 혼동 문자 공격을 모두 막지 못하므로 계정 식별자는 허용 문자·길이·중복 정책과 인가 검사를 함께 적용합니다. 비밀번호는 가입 때와 로그인 때 같은 명시적 규칙을 사용해야 하며, 기존 값을 임의로 정규화해 바꾸지 않습니다. 로그에는 원문 개인정보를 불필요하게 남기지 마세요.
[직접 해볼 과제]
`first`와 `second`를 NFD로 바꾼 뒤 코드 포인트를 출력해 보세요. 이어 두 값을 리스트에 넣고 NFC 정규화 후 집합 크기가 1인지 확인하고, NFKC를 사용자 입력 전체에 무조건 적용하면 안 되는 이유를 한 문장으로 적으세요.
[확인문제]
1. 화면에 같은 `가`가 그대로 비교하면 다를 수 있는 이유는 무엇입니까?
2. NFC와 NFKC를 선택할 때 확인해야 할 차이는 무엇입니까?
3. 정규화만으로 비슷하게 보이는 다른 문자의 공격을 막을 수 없는 이유는 무엇입니까?
[다음 학습]
다음에는 파일과 API에서 문자 인코딩을 명시하고 잘못된 바이트를 안전하게 처리하는 방법을 배웁니다.
[공식 참고 자료]
- Unicode 정규화 형식 UAX #15: https://www.unicode.org/reports/tr15/
- Unicode 보안 메커니즘 UTS #39: https://www.unicode.org/reports/tr39/
- Python unicodedata 공식 문서: https://docs.python.org/3/library/unicodedata.html
- ECMAScript String.prototype.normalize 명세: https://tc39.es/ecma262/multipage/text-processing.html#sec-string.prototype.normalize
- W3C 문자열 정규화 지침: https://www.w3.org/TR/charmod-norm/
댓글목록 0
등록된 댓글이 없습니다.
