PEP 672 – Python의 유니코드 관련 보안 고려 사항
- Author:
- Petr Viktorin <encukou at gmail.com>
- Status:
- Active
- Type:
- Informational
- Created:
- 01-Nov-2021
- Post-History:
- 01-Nov-2021
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 문서는 유니코드를 악용하여 실제로 하는 일과 다른 일을 하는 것처럼 보이는 Python 프로그램을 작성할 수 있는 방법들을 설명합니다.
이 문서는 어떠한 권고 사항이나 해결책도 제시하지 않습니다.
소개
Unicode는 모든 종류의 문자 언어를 다루기 위한 체계입니다. 유니코드는 인류의 모든 언어에 속한 어떤 문자든 사용할 수 있게 하는 것을 목표로 합니다. Python 코드는 유효한 유니코드 문자 거의 전부로 구성될 수 있습니다. 이는 전 세계의 프로그래머가 자신을 표현할 수 있게 해 주지만, 한편으로는 읽는 사람을 혼란스럽게 할 수 있는 코드를 작성하는 것도 가능하게 합니다.
Python의 유니코드 관련 기능을 악용하여 실제로 하는 일과 다른 일을 하는 것처럼 보이는 코드를 작성할 수 있습니다. 악의적인 사람은 이를 이용하여 코드 리뷰어를 속이고 악성 코드를 승인하게 만들 수 있습니다.
이러한 문제는 일반적으로 언어에 과도한 제약을 가하지 않고서는 Python 자체에서 해결할 수 없습니다. 이러한 문제는 코드 편집기와 리뷰 도구(diff 표시 등)에서, 프로젝트별 정책을 시행함으로써, 그리고 개별 프로그래머의 인식을 높임으로써 해결해야 합니다.
이 문서는 의도적으로 어떠한 해결책이나 권고 사항도 제시하지 않습니다. 이 문서는 오히려 염두에 두어야 할 사항의 목록입니다.
이 문서는 Python에 한정된 내용을 다룹니다. 유니코드 텍스트와 소스 코드에 관한 일반적인 보안 고려 사항은 유니코드 기술 보고서 [tr36], [tr39], [tr55]를 참고하십시오. (Python이 이 명세들을 반드시 준수하는 것은 아니라는 점에 유의하십시오.)
감사의 글
이 문서를 위한 조사는 Nicholas Boucher와 Ross Anderson이 보고한 CVE-2021-42574, Trojan Source Attacks에서 비롯되었습니다. 이 보고는 다양한 프로그래밍 언어에서의 양방향 재정의(Bidirectional override) 문자와 호모글리프(homoglyph)에 초점을 맞추고 있습니다.
혼란을 주는 기능
이 절에서는 예상 밖의 결과를 낳거나 악용될 수 있는 몇 가지 유니코드 관련 기능을 나열합니다.
ASCII에 한정된 고려 사항
ASCII는 유니코드의 부분 집합으로, 가장 흔히 쓰이는 기호, 숫자, 라틴 문자, 제어 문자로 구성됩니다.
ASCII 문자 집합의 문제는 일반적으로 잘 알려져 있지만, 비ASCII 사례를 더 잘 이해하는 데 도움이 되도록 여기에서 소개합니다.
혼동하기 쉬운 문자와 오타
어떤 문자들은 서로 비슷하게 생겼습니다. 컴퓨터 시대 이전에는 많은 기계식 타자기에 숫자 0과 1 키가 없었습니다. 그래서 사용자들은 그 대신 O(대문자 o)와 l(소문자 L)을 입력했습니다. 사람은 오직 문맥만으로 이들을 구별할 수 있었습니다. 그러나 프로그래밍 언어에서는 숫자와 문자의 구별이 매우 중요합니다. 그래서 프로그래머를 위해 설계된 대부분의 글꼴은 이들을 쉽게 구별할 수 있게 해 줍니다.
마찬가지로, 사람의 언어를 위해 설계된 글꼴에서는 대문자 “I”와 소문자 “l”이 비슷해 보일 수 있습니다. 또는 “rn”이라는 글자들이 한 글자 “m”과 사실상 구별되지 않을 수도 있습니다. 이 경우에도 프로그래머용 글꼴은 이처럼 혼동하기 쉬운 문자(confusables) 쌍을 눈에 띄게 다르게 표시합니다.
그러나 무엇이 “눈에 띄게” 다른지는 항상 맥락에 따라 달라집니다. 사람은 긴 식별자의 세부 사항을 무시하는 경향이 있습니다. 변수 이름 accessibi1ity_options는 여전히 accessibility_options와 구별되지 않아 보일 수 있지만, 컴파일러에게는 서로 다른 식별자입니다. 단순한 오타도 마찬가지입니다. 대부분의 사람은 responsbility_chain_delegate에 있는 오타를 알아차리지 못할 것입니다.
제어 문자
Python은 일반적으로 모든 CR (\r), LF (\n), CR-LF 쌍(\r\n)을 줄 끝 문자로 간주합니다. 대부분의 코드 편집기도 마찬가지이지만, “네이티브가 아닌” 줄 끝을 만나면 줄을 끝내지 않고 알 수 없는 문자로 표시하거나 아예 아무것도 표시하지 않는 편집기도 있습니다. 이런 편집기는 다음 예제를:
# Don't call this function:
fire_the_missiles()
다음과 같은 무해한 주석으로 표시합니다.:
# Don't call this function:⬛fire_the_missiles()
CPython은 제어 문자 NUL (\0)을 입력의 끝으로 처리할 수 있지만, 많은 편집기는 이 문자를 그냥 건너뛰므로 파이썬이 실행하지 않을 코드를 파일의 일반적인 부분처럼 보여 줄 수 있습니다.
일부 문자는 일반적인 터미널에 소스를 출력할 때 다른 문자를 숨기거나 덮어쓰는 데 사용될 수 있습니다. 예를 들면 다음과 같습니다.
- BS (
\b, 백스페이스)는 커서를 뒤로 이동시키므로, 그 뒤의 문자가 앞의 문자를 덮어씁니다. - CR (
\r, 캐리지 리턴)은 커서를 줄의 시작으로 이동시키므로, 이후의 문자가 줄의 시작 부분을 덮어씁니다. - SUB (
\x1A, Ctrl+Z)는 Windows에서 “텍스트의 끝”을 의미합니다. 일부 프로그램(예:type)은 이 문자 뒤에 오는 파일의 나머지 부분을 무시합니다. - ESC (
\x1B)는 흔히 터미널을 임의로 제어할 수 있는 이스케이프 코드를 시작합니다.
식별자에서 혼동하기 쉬운 문자
Python은 ASCII에 국한되지 않습니다. 파이썬은 라틴 문자부터 고대 이집트 상형 문자까지 모든 문자 체계의 문자를 식별자(변수 이름 등)에 허용합니다. 자세한 내용과 근거는 PEP 3131을 참조하십시오. “문자와 숫자”만 허용되므로 γάτα는 유효한 Python 식별자이지만 🐱는 그렇지 않습니다. (자세한 내용은 Identifiers and keywords를 참조하십시오.)
출력되지 않는 제어 문자도 식별자에 허용되지 않습니다.
그러나 허용되는 문자 집합 안에도 “혼동하기 쉬운 문자”가 매우 많습니다. 예를 들어, 라틴 문자 b, 그리스 문자 β (베타), 키릴 문자 в (베)의 대문자는 흔히 똑같아 보입니다. 각각 B, Β, В입니다.
이로 인해 사람에게는 똑같아 보이지만 Python에게는 그렇지 않은 식별자가 허용됩니다. 예를 들어, 다음은 모두 서로 다른 식별자입니다.
scope(라틴 문자, ASCII만 사용)scоpe(키릴 문자о사용)scοpe(그리스 문자ο사용)ѕсоре(모두 키릴 문자)
또한 일부 글자는 글자가 아닌 것처럼 보일 수 있습니다.
- 하와이어 ʻokina를 나타내는 문자는 아포스트로피처럼 보입니다.
ʻHelloʻ는 문자열이 아니라 Python 식별자입니다. - 동아시아에서 열을 뜻하는 단어는 더하기 기호처럼 보이므로
十= 10은 완전한 Python 문장입니다. (“十”는 “10”이 아니라 “열”이라는 단어입니다.)
Note
그 반대도 마찬가지로, 일부 기호는 문자처럼 보입니다. 하지만 Python은 식별자에 임의의 기호를 허용하지 않으므로 이는 문제가 되지 않습니다.
혼동하기 쉬운 숫자
Python의 숫자 리터럴은 ASCII 숫자 0-9(그리고 .이나 e와 같은 숫자가 아닌 문자)만 사용합니다.
하지만 int와 float 생성자나 str.format 메서드처럼 문자열에서 숫자로 변환할 때는 모든 십진 숫자를 사용할 수 있습니다. 예를 들어 ߅(NKO DIGIT FIVE)나 ௫(TAMIL DIGIT FIVE)는 숫자 5로 동작합니다.
일부 문자 체계에는 ASCII 숫자와 비슷해 보이지만 값이 다른 숫자가 있습니다. 예를 들면 다음과 같습니다.:
>>> int('৪୨')
42
>>> '{٥}'.format('zero', 'one', 'two', 'three', 'four', 'five')
five
양방향 텍스트
히브리어나 아랍어 같은 일부 문자 체계는 오른쪽에서 왼쪽으로 씁니다. 이러한 문자 체계로 쓴 구절은 주변 텍스트와 상호 작용하는데, 그 방식은 이러한 문자 체계와 그 컴퓨터 표현에 익숙하지 않은 사람에게 놀라울 수 있습니다.
정확한 처리 과정은 복잡하며, 유니코드 표준 부속서 #9인 Unicode Bidirectional Algorithm에 설명되어 있습니다.
변수 s에 100자짜리 문자열을 할당하는 다음 코드를 살펴봅시다.:
s = "X" * 100 # "X" is assigned
X를 히브리 문자 א로 바꾸면 이 줄은 다음과 같이 됩니다.:
s = "א" * 100 # "א" is assigned
이 명령은 여전히 s에 100자짜리 문자열을 할당하지만, 양방향 알고리즘에 따라 일반 텍스트로 표시하면(예: 브라우저에서) s = "א" 뒤에 주석이 이어지는 것처럼 보입니다.
그 밖의 놀라운 예는 다음과 같습니다.
ערך = 23문에서 변수ערך에는 정수 23이 설정됩니다.قيمة = ערך문에서 변수قيمة에는ערך의 값이 설정됩니다.قيمة - (ערך ** 2)문에서는ערך의 값을 제곱한 다음قيمة에서 뺍니다. 여는 괄호는)로 표시됩니다.
양방향 표시, 임베딩, 오버라이드 및 아이솔레이트
기본 재배열 규칙이 항상 의도한 텍스트 방향을 만들어 내지는 않으므로, 유니코드는 이를 바꾸는 여러 방법을 제공합니다.
가장 기본적인 것은 방향 표시로, 눈에 보이지 않지만 왼쪽에서 오른쪽으로 쓰는(또는 오른쪽에서 왼쪽으로 쓰는) 문자와 같은 방식으로 텍스트에 영향을 줍니다. 위의 s = "X" 예제에 이어서, 다음 예제에서는 X를 오른쪽에서 왼쪽 표시(U+200F)가 앞이나 뒤에 붙은 라틴 문자 x로 바꿉니다. 이 코드는 s에 200자짜리 문자열(x 100개 사이사이에 보이지 않는 표시 100개가 섞인 문자열)을 할당하지만, 일반 텍스트에 대한 유니코드 규칙에 따르면 s = "x" 뒤에 ASCII로만 된 주석이 이어지는 것처럼 렌더링됩니다.:
s = "x" * 100 # "x" is assigned
방향성 임베딩, 오버라이드, 격리 문자도 보이지 않지만, 전용 문자로 끝나거나 줄 끝에 이를 때까지 그 뒤에 오는 모든 텍스트의 순서에 영향을 줍니다. (유니코드는 그 효과가 “문단”의 끝까지 지속된다고 명시하지만(Unicode Bidirectional Algorithm 참조), 도구가 줄바꿈 문자를 문단의 끝으로 해석하는 것을 허용합니다(유니코드 Newline Guidelines 참조). 대부분의 코드 편집기와 터미널이 그렇게 합니다.)
이러한 문자는 사실상 그 뒤에 오는 텍스트를 임의로 재배열할 수 있게 합니다. Python은 이러한 문자를 문자열과 주석에서만 허용하므로 그 잠재적 영향이 제한되기는 하지만(특히 Python의 주석이 항상 줄 끝까지 이어진다는 사실과 결합하면 더욱 그렇습니다), 그렇다고 해서 무해해지는 것은 아닙니다.
식별자 정규화
Python 문자열은 “문자”가 아니라 유니코드 코드 포인트의 모음입니다.
이전 인코딩과의 호환성 같은 이유로, 유니코드에는 본질적으로 하나의 “문자”인 것을 인코딩하는 방법이 여러 가지인 경우가 많습니다. 예를 들어 다음은 모두 Å를 Python 문자열로 쓰는 서로 다른 방법이며, 각각은 나머지와 같지 않습니다.
"\N{LATIN CAPITAL LETTER A WITH RING ABOVE}"(코드 포인트 1개)"\N{LATIN CAPITAL LETTER A}\N{COMBINING RING ABOVE}"(코드 포인트 2개)"\N{ANGSTROM SIGN}"(코드 포인트 1개이지만 다른 문자)
또 다른 예로, 합자 fi는 두 글자 fi와 의미가 같지만 전용 유니코드 코드 포인트를 가지고 있습니다.
또한 흔히 쓰이는 글자에는 서로 구별되는 변형이 여러 개 있는 경우가 많습니다. 유니코드는 수학처럼 그 차이가 의미상 중요한 맥락을 위해 이러한 변형을 제공합니다. 예를 들어, n의 몇 가지 변형은 다음과 같습니다:
n(LATIN SMALL LETTER N)𝐧(MATHEMATICAL BOLD SMALL N)𝘯(MATHEMATICAL SANS-SERIF ITALIC SMALL N)n(FULLWIDTH LATIN SMALL LETTER N)ⁿ(SUPERSCRIPT LATIN SMALL LETTER N)
유니코드에는 이와 같은 변형을 하나의 형태로 정규화하는 알고리즘이 포함되어 있으며, Python 식별자는 정규화됩니다. (정규형에는 여러 가지가 있으며, Python은 NFKC를 사용합니다.)
예를 들어, xn과 xⁿ은 Python에서 같은 식별자입니다:
>>> xⁿ = 8
>>> xn
8
… fi와 fi도 마찬가지이며, Å를 인코딩하는 여러 방식도 마찬가지입니다.
하지만 이 정규화는 식별자에만 적용됩니다. getattr처럼 문자열을 식별자로 다루는 함수는 정규화를 수행하지 않습니다:
>>> class Test:
... def finalize(self):
... print('OK')
...
>>> Test().finalize()
OK
>>> Test().finalize()
OK
>>> getattr(Test(), 'finalize')
Traceback (most recent call last):
...
AttributeError: 'Test' object has no attribute 'finalize'
이는 임포트할 때에도 적용됩니다:
import finalization은 정규화를 수행하며,finalization.py라는 이름의 파일(및 기타finalization.*파일)을 찾습니다.importlib.import_module("finalization")은 정규화하지 않으므로,finalization.py라는 이름의 파일을 찾습니다.
일부 파일 시스템은 자체적으로 정규화나 대소문자 폴딩, 또는 둘 다를 적용합니다. 어떤 시스템에서는 finalization.py, finalization.py, FINALIZATION.py가 서로 다른 세 개의 파일 이름이지만, 다른 시스템에서는 이 중 일부 또는 전부가 같은 파일을 가리킵니다.
소스 인코딩
Python 소스 파일의 인코딩은 Encoding declarations에 따라 파일의 처음 두 줄에 대한 특정 정규식으로 지정됩니다. 이 메커니즘은 허용하는 범위가 매우 넓어서 난독화하기 쉽습니다.
이는 Python 전용 특수 목적 인코딩과 결합하여 악용될 수 있습니다(Text Encodings 참조). 예를 들어, encoding: unicode_escape를 사용하면 따옴표나 중괄호 같은 문자를 (f-)문자열 안에 숨길 수 있으며, 많은 도구(문법 하이라이터, 린터 등)는 이를 문자열의 일부로 간주합니다. 예를 들면 다음과 같습니다:
# For writing Japanese, you don't need an editor that supports
# UTF-8 source encoding: unicode_escape sequences work just as well.
import os
message = '''
This is "Hello World" in Japanese:
\u3053\u3093\u306b\u3061\u306f\u7f8e\u3057\u3044\u4e16\u754c
This runs `echo WHOA` in your shell:
\u0027\u0027\u0027\u002c\u0028\u006f\u0073\u002e
\u0073\u0079\u0073\u0074\u0065\u006d\u0028
\u0027\u0065\u0063\u0068\u006f\u0020\u0057\u0048\u004f\u0041\u0027
\u0029\u0029\u002c\u0027\u0027\u0027
'''
여기서 첫 주석에 있는 encoding: unicode_escape는 인코딩 선언입니다. unicode_escape 인코딩은 Python이 \u0027을 작은따옴표(문자열을 시작하거나 끝낼 수 있음)로, \u002c를 쉼표(구두점)로 처리하는 등의 동작을 하도록 지시합니다.
미해결 문제
아마도 다음을 작성하여 공개해야 할 것입니다:
- 텍스트 편집기와 코드 도구를 위한 권장 사항
- 프로그래머와 팀을 위한 권장 사항
- Python에서 가능한 개선 사항
참고 문헌
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.