Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 672 – Python의 유니코드 관련 보안 고려 사항

Author:
Petr Viktorin <encukou at gmail.com>
Status:
Active
Type:
Informational
Created:
01-Nov-2021
Post-History:
01-Nov-2021

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 문서는 실제로 하는 일과 다른 일을 하는 것처럼 보이는 Python 프로그램을 작성하기 위해 Unicode를 오용할 수 있는 방법을 설명합니다.

이 문서는 어떠한 권고 사항이나 해결책도 제시하지 않습니다.

서론

Unicode는 모든 종류의 문자 언어를 처리하기 위한 시스템입니다. 모든 인간 언어의 모든 문자를 사용할 수 있도록 하는 것을 목표로 합니다. Python 코드는 거의 모든 유효한 Unicode 문자로 구성될 수 있습니다. 이는 전 세계 프로그래머가 자신을 표현할 수 있도록 하지만, 독자에게 혼란을 줄 가능성이 있는 코드를 작성할 수 있도록 하기도 합니다.

Python의 Unicode 관련 기능을 오용하여 실제로 하는 일과 다른 일을 하는 것처럼 보이는 코드를 작성할 수 있습니다. 악의적인 행위자는 이를 이용하여 코드 검토자가 악성 코드를 받아들이도록 속일 수 있습니다.

가능한 문제는 일반적으로 언어에 과도한 제한을 가하지 않고는 Python 자체에서 해결할 수 없습니다. 이러한 문제는 코드 편집기와 검토 도구(예: diff 표시)에서 프로젝트별 정책을 적용하고 개별 프로그래머의 인식을 높임으로써 해결해야 합니다.

이 문서는 의도적으로 어떠한 해결책이나 권고 사항도 제시하지 않으며, 유념해야 할 사항의 목록에 가깝습니다.

이 문서는 Python에 한정됩니다. Unicode 텍스트와 소스 코드에 대한 일반적인 보안 고려 사항은 Unicode 기술 보고서 [tr36], [tr39], [tr55]를 참조하십시오. (Python이 이러한 사양을 반드시 준수하는 것은 아닙니다.)

감사의 글

이 문서에 대한 조사는 Nicholas Boucher와 Ross Anderson이 보고한 CVE-2021-42574, Trojan Source Attacks에서 비롯되었으며, 이 보고서는 다양한 프로그래밍 언어에서 양방향 재정의 문자와 동형 문자를 중점적으로 다룹니다.

혼동을 일으키는 기능

이 절에서는 놀랍거나 오용될 수 있는 Unicode 관련 기능을 몇 가지 나열합니다.

ASCII 전용 고려 사항

ASCII는 가장 일반적인 기호, 숫자, 라틴 문자 및 제어 문자로 구성된 Unicode의 부분 집합입니다.

ASCII 문자 집합과 관련된 문제는 일반적으로 잘 알려져 있지만, 비ASCII 사례를 더 잘 이해할 수 있도록 여기에서 제시합니다.

혼동하기 쉬운 문자와 오타

일부 문자는 서로 비슷하게 보입니다. 컴퓨터 시대 이전에는 많은 기계식 타자기에 숫자 01에 해당하는 키가 없었으므로, 사용자들은 대신 O(대문자 o)와 l(소문자 L)을 입력했습니다. 사람은 문맥만으로 이들을 구별할 수 있었습니다. 그러나 프로그래밍 언어에서는 숫자와 문자의 구별이 매우 중요하며, 프로그래머를 위해 설계된 대부분의 글꼴은 이들을 쉽게 구별할 수 있도록 합니다.

마찬가지로 인간 언어를 위해 설계된 글꼴에서는 대문자 “I”와 소문자 “l”이 비슷하게 보일 수 있습니다. 또는 문자 “rn”이 단일 문자 “m”과 사실상 구별되지 않을 수도 있습니다. 다시 말해, 프로그래머용 글꼴은 이러한 혼동하기 쉬운 문자 쌍을 눈에 띄게 다르게 표시합니다.

그러나 무엇이 “눈에 띄게” 다른지는 항상 맥락에 따라 달라집니다. 사람은 긴 식별자의 세부 사항을 무시하는 경향이 있습니다. 변수 이름 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)을 입력의 끝으로 처리할 수 있지만, 많은 편집기는 이를 단순히 건너뛰므로 Python이 파일의 일반적인 일부로 실행하지 않을 코드를 표시할 수도 있습니다.

일반적인 터미널에 소스 코드가 나열될 때 일부 문자를 사용하여 다른 문자를 숨기거나 덮어쓸 수 있습니다. 예를 들면 다음과 같습니다.

  • BS (\b, 백스페이스)는 커서를 뒤로 이동하므로 그 뒤의 문자가 앞의 문자를 덮어씁니다.
  • CR (\r, 캐리지 리턴)은 커서를 줄의 시작으로 이동하며, 이후 문자는 줄의 시작 부분을 덮어씁니다.
  • SUB (\x1A, Ctrl+Z)는 Windows에서 “텍스트 끝”을 의미합니다. 일부 프로그램(예: type)은 그 뒤에 있는 파일의 나머지 부분을 무시합니다.
  • ESC (\x1B)는 일반적으로 터미널을 임의로 제어할 수 있는 이스케이프 코드를 시작합니다.

식별자에서 혼동하기 쉬운 문자

Python은 ASCII로 제한되지 않습니다. Python에서는 식별자(예: 변수 이름)에 모든 문자 체계의 문자를 사용할 수 있으며, 라틴 문자부터 고대 이집트 상형문자까지 포함됩니다. 자세한 내용과 근거는 PEP 3131을 참조하십시오. “문자와 숫자”만 허용되므로 γάτα는 유효한 Python 식별자이지만 🐱는 그렇지 않습니다. (자세한 내용은 Identifiers and keywords를 참조하십시오.)

인쇄되지 않는 제어 문자도 식별자에 사용할 수 없습니다.

그러나 허용되는 문자 집합 안에는 “혼동하기 쉬운 문자”가 매우 많습니다. 예를 들어 라틴 문자 b, 그리스 문자 β(베타), 키릴 문자 в(베)의 대문자 버전은 흔히 똑같아 보입니다. 각각 B, Β, В입니다.

이로 인해 사람에게는 똑같아 보이지만 Python에서는 그렇지 않은 식별자를 만들 수 있습니다. 예를 들어 다음 식별자들은 모두 서로 다릅니다.

  • scope (라틴 문자, ASCII만 사용)
  • scоpe (키릴 문자 о 포함)
  • scοpe (그리스 문자 ο 포함)
  • ѕсоре (모두 키릴 문자)

또한 일부 문자는 문자가 아닌 것처럼 보일 수 있습니다.

  • 하와이어 ʻokina 문자는 아포스트로피처럼 보입니다. ʻHelloʻ는 문자열이 아니라 Python 식별자입니다.
  • 동아시아에서 ten을 나타내는 단어는 더하기 기호처럼 보이므로 十= 10은 완전한 Python 문입니다. (“十”은 “10”이 아니라 “ten”이라는 단어입니다.)

Note

반대의 경우도 적용됩니다. 일부 기호는 문자처럼 보이지만, Python에서는 식별자에 임의의 기호를 사용할 수 없으므로 문제가 되지 않습니다.

혼동하기 쉬운 숫자

Python의 숫자 리터럴은 ASCII 숫자 0-9(그리고 .e 같은 숫자가 아닌 문자)만 사용합니다.

그러나 intfloat 생성자나 str.format 메서드에서처럼 문자열에서 숫자를 변환할 때는 모든 십진 숫자를 사용할 수 있습니다. 예를 들어 ߅(NKO DIGIT FIVE) 또는 (TAMIL DIGIT FIVE)는 숫자 5로 작동합니다.

일부 문자 체계에는 ASCII 숫자와 비슷하게 보이지만 값이 다른 숫자가 포함되어 있습니다. 예를 들어 다음과 같습니다.:

>>> int('৪୨')
42
>>> '{٥}'.format('zero', 'one', 'two', 'three', 'four', 'five')
five

양방향 텍스트

히브리어나 아랍어와 같은 일부 문자 체계는 오른쪽에서 왼쪽으로 작성됩니다. 이러한 문자 체계의 구문은 해당 문자 체계와 컴퓨터 표현에 익숙하지 않은 사람에게는 주변 텍스트와 상호 작용하는 방식이 놀라울 수 있습니다.

정확한 과정은 복잡하며 Unicode Standard Annex #9, Unicode Bidirectional Algorithm에 설명되어 있습니다.

다음 코드를 살펴보십시오. 이 코드는 100자 문자열을 변수 s에 할당합니다.:

s = "X" * 100 #    "X" is assigned

X를 히브리어 문자 א로 바꾸면 줄은 다음과 같이 됩니다.:

s = "א" * 100 #    "א" is assigned

이 명령은 여전히 100자 문자열을 s에 할당하지만, 양방향 알고리즘에 따라 일반 텍스트로 표시하면(예를 들어 브라우저에서) s = "א" 뒤에 주석이 오는 것처럼 표시됩니다.

그 밖의 놀라운 예는 다음과 같습니다.

  • ערך = 23 문에서 변수 ערך는 정수 23으로 설정됩니다.
  • قيمة = ערך 문에서 변수 قيمةערך의 값으로 설정됩니다.
  • قيمة - (ערך ** 2) 문에서는 ערך의 값이 제곱된 후 قيمة에서 빼집니다. 여는 괄호가 )로 표시됩니다.

양방향 표시, 임베딩, 재정의 및 격리

기본 재배치 규칙이 항상 의도한 텍스트 방향을 만들어 내는 것은 아니므로 Unicode는 이를 변경하는 여러 방법을 제공합니다.

가장 기본적인 것은 directional marks입니다. 이러한 표시는 보이지 않지만 왼쪽에서 오른쪽(또는 오른쪽에서 왼쪽) 문자처럼 텍스트에 영향을 줍니다. 앞의 s = "X" 예를 계속 살펴보면, 다음 예에서는 X가 라틴 문자 x로 대체되고 그 뒤나 앞에 오른쪽에서 왼쪽 표시(U+200F)가 배치됩니다. 이렇게 하면 s에 200자 문자열이 할당됩니다(x 100개 사이에 보이지 않는 표시 100개가 삽입됩니다). 하지만 일반 텍스트에 대한 Unicode 규칙에 따라 ASCII만 포함된 주석이 뒤따르는 s = "x"로 렌더링됩니다.:

s = "x‏" * 100 #    "‏x" is assigned

방향성 embedding, overrideisolate 문자는 역시 보이지 않지만, 전용 문자로 종료되거나 줄 끝에 이를 때까지 그 뒤에 오는 모든 텍스트의 순서에 영향을 줍니다. (유니코드는 그 효과가 “단락”의 끝까지 지속되도록 규정하지만( Unicode Bidirectional Algorithm 참조), 도구가 줄 바꿈 문자를 단락의 끝으로 해석할 수 있도록 허용합니다(유니코드의 Newline Guidelines를 참조하십시오). 대부분의 코드 편집기와 터미널은 그렇게 해석합니다.) 대부분의 코드 편집기와 터미널이 그렇게 합니다.)

이러한 문자는 본질적으로 그 뒤에 오는 텍스트를 임의로 재배치할 수 있게 합니다. Python은 문자열과 주석에서만 이러한 문자를 허용하므로 그 잠재력은 제한됩니다(특히 Python의 주석이 항상 줄 끝까지 이어진다는 점과 결합될 때 그러합니다). 하지만 그렇다고 해서 이러한 문자가 무해해지는 것은 아닙니다.

식별자 정규화

Python 문자열은 “문자”가 아니라 Unicode codepoints의 모음입니다.

이전 인코딩과의 호환성 같은 이유로 Unicode에는 본질적으로 하나의 “문자”인 것을 인코딩하는 여러 방법이 있는 경우가 많습니다. 예를 들어 다음은 모두 Å를 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와 의미는 같습니다.

또한 일반적인 문자에는 서로 구별되는 여러 변형이 자주 존재합니다. 유니코드는 수학과 같이 이러한 차이가 어떤 의미를 갖는 문맥을 위해 이러한 변형을 제공합니다. 예를 들어 n의 일부 변형은 다음과 같습니다:

  • n (LATIN SMALL LETTER N)
  • 𝐧 (MATHEMATICAL BOLD SMALL N)
  • 𝘯 (MATHEMATICAL SANS-SERIF ITALIC SMALL N)
  • (FULLWIDTH LATIN SMALL LETTER N)
  • (SUPERSCRIPT LATIN SMALL LETTER N)

유니코드에는 이와 같은 변형을 하나의 형식으로 정규화하는 알고리즘이 포함되어 있으며, Python 식별자도 정규화됩니다. (정규 형식은 여러 가지가 있으며, Python은 NFKC를 사용합니다.)

예를 들어 Python에서 xnxⁿ은 동일한 식별자입니다.:

>>> xⁿ = 8
>>> xn
8

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.pyFINALIZATION.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에서 가능한 개선 사항

참고 문헌

[tr36]
유니코드 기술 보고서 #36: 유니코드 보안 고려 사항 http://www.unicode.org/reports/tr36/
[tr39]
유니코드® 기술 표준 #39: 유니코드 보안 메커니즘 http://www.unicode.org/reports/tr39/
[tr55]
유니코드 기술 보고서 #55: 유니코드 소스 코드 처리 http://www.unicode.org/reports/tr55/