PEP 672 – Unicode-bezogene Sicherheitsüberlegungen für Python
- Autor:
- Petr Viktorin <encukou at gmail.com>
- Status:
- Aktiv
- Typ:
- Informational
- Erstellt:
- 01-Nov-2021
- Post-History:
- 01-Nov-2021
Inhaltsverzeichnis
- Zusammenfassung
- Einleitung
- Danksagung
- Verwirrende Funktionen
- Offene Fragen
- Referenzen
- Urheberrecht
Zusammenfassung
Dieses Dokument erklärt mögliche Wege, wie Unicode missbraucht werden kann, um Python-Programme zu schreiben, die etwas anderes zu sein scheinen, als sie tatsächlich sind.
Dieses Dokument gibt keine Empfehlungen und Lösungen.
Einleitung
Unicode ist ein System zur Verarbeitung aller Arten von Schriftsprache. Es zielt darauf ab, die Verwendung jedes Zeichens aus jeder menschlichen Sprache zu ermöglichen. Python-Code kann aus fast allen gültigen Unicode-Zeichen bestehen. Dies ermöglicht es Programmierern aus aller Welt, sich auszudrücken, birgt aber auch die Gefahr, Code zu schreiben, der für Leser potenziell verwirrend ist.
Es ist möglich, die Unicode-bezogenen Funktionen von Python zu missbrauchen, um Code zu schreiben, der etwas anderes *zu sein scheint*, als er tut. Böswillige Akteure könnten dies ausnutzen, um Code-Prüfer dazu zu bringen, bösartigen Code zu akzeptieren.
Die möglichen Probleme können in Python selbst im Allgemeinen nicht ohne übermäßige Einschränkungen der Sprache gelöst werden. Sie sollten in Code-Editoren und Überprüfungswerkzeugen (wie z. B. diff-Anzeigen) gelöst werden, indem projektspezifische Richtlinien durchgesetzt und das Bewusstsein einzelner Programmierer geschärft wird.
Dieses Dokument gibt bewusst keine Lösungen oder Empfehlungen an: Es ist vielmehr eine Liste von Dingen, die man beachten sollte.
Dieses Dokument ist spezifisch für Python. Allgemeine Sicherheitsüberlegungen zu Unicode-Text und Quellcode finden Sie in den Unicode Technical Reports [tr36], [tr39] und [tr55]. (Beachten Sie, dass Python diesen Spezifikationen nicht unbedingt entspricht.)
Danksagung
Die Untersuchung für dieses Dokument wurde durch CVE-2021-42574, Trojan Source Attacks, ausgelöst, die von Nicholas Boucher und Ross Anderson gemeldet wurden und sich auf bidirektionale Überschreibungszeichen und Homoglyphen in verschiedenen Programmiersprachen konzentrieren.
Verwirrende Funktionen
Dieser Abschnitt listet einige Unicode-bezogene Funktionen auf, die überraschend oder missbrauchbar sein können.
Überlegungen nur für ASCII
ASCII ist eine Teilmenge von Unicode und besteht aus den gebräuchlichsten Symbolen, Zahlen, lateinischen Buchstaben und Steuerzeichen.
Während die Probleme mit dem ASCII-Zeichensatz im Allgemeinen gut verstanden sind, werden sie hier aufgeführt, um die nicht-ASCII-Fälle besser zu verstehen.
Verwechslungsgefahren und Tippfehler
Einige Zeichen sehen ähnlich aus. Vor dem Zeitalter der Computer fehlten vielen mechanischen Schreibmaschinen die Tasten für die Ziffern 0 und 1: Anstelle dessen tippten Benutzer O (großes O) und l (kleines L). Menschliche Leser konnten sie anhand des Kontexts unterscheiden. In Programmiersprachen ist die Unterscheidung zwischen Ziffern und Buchstaben jedoch entscheidend – und die meisten Schriftarten, die für Programmierer entwickelt wurden, machen es leicht, sie auseinanderzuhalten.
Ähnlich können in für menschliche Sprachen entwickelten Schriftarten das große "I" und das kleine "l" ähnlich aussehen. Oder die Buchstaben "rn" können von dem einzelnen Buchstaben "m" praktisch nicht zu unterscheiden sein. Auch hier machen Schriftarten für Programmierer diese Paare von *verwechslungsgefährdeten Zeichen* merklich unterschiedlich.
Was "merklich" unterschiedlich ist, hängt jedoch immer vom Kontext ab. Menschen neigen dazu, Details in längeren Bezeichnern zu ignorieren: Der Variablenname accessibi1ity_options kann immer noch ununterscheidbar aussehen wie accessibility_options, obwohl sie für den Compiler unterschiedlich sind. Dasselbe gilt für einfache Tippfehler: Die meisten Menschen werden den Tippfehler in responsbility_chain_delegate nicht bemerken.
Steuerzeichen
Python behandelt im Allgemeinen alle CR (\r), LF (\n) und CR-LF-Paare (\r\n) als Zeilenendezeichen. Die meisten Code-Editoren tun dies ebenfalls, aber es gibt Editoren, die "nicht-native" Zeilenenden als unbekannte Zeichen (oder gar nichts) anzeigen, anstatt die Zeile zu beenden, und dieses Beispiel anzeigen
# Don't call this function:
fire_the_missiles()
als harmlosen Kommentar wie
# Don't call this function:⬛fire_the_missiles()
CPython kann das Steuerzeichen NUL (\0) als Eingabeende behandeln, aber viele Editoren überspringen es einfach und zeigen möglicherweise Code, den Python nicht ausführt, als regulären Teil einer Datei an.
Einige Zeichen können verwendet werden, um andere Zeichen zu verbergen/überschreiben, wenn die Quelle in gängigen Terminals aufgelistet wird. Zum Beispiel
- BS (
\b, Backspace) bewegt den Cursor zurück, sodass das Zeichen danach das vorherige Zeichen überschreibt. - CR (
\r, Wagenrücklauf) bewegt den Cursor zum Anfang der Zeile, nachfolgende Zeichen überschreiben den Anfang der Zeile. - SUB (
\x1A, Strg+Z) bedeutet "Textende" unter Windows. Einige Programme (wietype) ignorieren den Rest der Datei danach. - ESC (
\x1B) leitet üblicherweise Escape-Sequenzen ein, die eine beliebige Steuerung des Terminals ermöglichen.
Verwechslungsgefährdete Zeichen in Bezeichnern
Python ist nicht auf ASCII beschränkt. Es erlaubt Zeichen aller Schriftsysteme – lateinische Buchstaben bis hin zu altägyptischen Hieroglyphen – in Bezeichnern (wie Variablennamen). Einzelheiten und Begründungen finden Sie in PEP 3131. Nur "Buchstaben und Zahlen" sind erlaubt, daher ist γάτα ein gültiger Python-Bezeichner, 🐱 jedoch nicht. (Einzelheiten finden Sie unter Bezeichner und Schlüsselwörter.)
Nicht druckbare Steuerzeichen sind ebenfalls nicht in Bezeichnern erlaubt.
Innerhalb des erlaubten Satzes gibt es jedoch eine große Anzahl von "verwechslungsgefährdeten Zeichen". Zum Beispiel sehen die Großbuchstaben des lateinischen b, des griechischen β (Beta) und des kyrillischen в (Ve) oft identisch aus: B, Β und В.
Dies ermöglicht Bezeichner, die für Menschen gleich aussehen, aber nicht für Python. Zum Beispiel sind alle folgenden Bezeichner unterschiedlich
scope(Lateinisch, nur ASCII)scоpe(mit kyrillischemо)scοpe(mit griechischemο)ѕсоре(alle kyrillischen Buchstaben)
Zusätzlich können einige Buchstaben wie Nicht-Buchstaben aussehen
- Der Buchstabe für das hawaiianische ʻokina sieht aus wie ein Apostroph;
ʻHelloʻist ein Python-Bezeichner, keine Zeichenkette. - Das ostasiatische Wort für zehn sieht aus wie ein Pluszeichen, daher ist
十= 10eine vollständige Python-Anweisung. (Das "十" ist ein Wort: "zehn" und nicht "10".)
Hinweis
Das Gegenteil gilt auch – einige Symbole sehen wie Buchstaben aus –, aber da Python keine beliebigen Symbole in Bezeichnern zulässt, ist dies kein Problem.
Verwechslungsgefährdete Ziffern
Numerische Literale in Python verwenden nur die ASCII-Ziffern 0-9 (und Nicht-Ziffern wie . oder e).
Wenn Zahlen jedoch aus Zeichenketten konvertiert werden, wie bei den Konstruktoren int und float oder über die Methode str.format, kann jede Dezimalziffer verwendet werden. Zum Beispiel funktionieren ߅ (NKO DIGIT FIVE) oder ௫ (TAMIL DIGIT FIVE) wie die Ziffer 5.
Einige Schriftsysteme enthalten Ziffern, die ASCII-Ziffern ähneln, aber einen anderen Wert haben. Zum Beispiel
>>> int('৪୨')
42
>>> '{٥}'.format('zero', 'one', 'two', 'three', 'four', 'five')
five
Bidirektionaler Text
Einige Schriftsysteme, wie Hebräisch oder Arabisch, werden von rechts nach links geschrieben. Phrasen in solchen Schriftsystemen interagieren mit umliegendem Text auf eine Weise, die für Personen, die mit diesen Schriftsystemen und ihrer Computerrepräsentation nicht vertraut sind, überraschend sein kann.
Der genaue Prozess ist kompliziert und im Unicode Standard Annex #9, Unicode Bidirectional Algorithm, erklärt.
Betrachten Sie den folgenden Code, der eine 100 Zeichen lange Zeichenkette der Variablen s zuweist
s = "X" * 100 # "X" is assigned
Wenn das X durch den hebräischen Buchstaben א ersetzt wird, wird die Zeile zu
s = "א" * 100 # "א" is assigned
Dieser Befehl weist s immer noch eine 100 Zeichen lange Zeichenkette zu, aber wenn sie als allgemeiner Text gemäß dem bidirektionalen Algorithmus angezeigt wird (z. B. in einem Browser), erscheint sie als s = "א" gefolgt von einem Kommentar.
Andere überraschende Beispiele sind
- In der Anweisung
ערך = 23wird die Variableערךauf die Ganzzahl 23 gesetzt. - In der Anweisung
قيمة = ערךwird die Variableقيمةauf den Wert vonערךgesetzt. - In der Anweisung
قيمة - (ערך ** 2)wird der Wert vonערךquadriert und dann vonقيمةsubtrahiert. Die *öffnende* Klammer wird als)angezeigt.
Bidirektionale Markierungen, Einbettungen, Überschreibungen und Isolate
Die Standard-Neuordnungsregeln ergeben nicht immer die beabsichtigte Textrichtung, daher bietet Unicode verschiedene Möglichkeiten, diese zu ändern.
Die grundlegendsten sind **Richtungsmarkierungen**, die unsichtbar sind, aber den Text wie ein links-nach-rechts- (oder rechts-nach-links-) Zeichen beeinflussen. Weiter im Beispiel s = "X" oben, wird im nächsten Beispiel das X durch das lateinische x ersetzt, gefolgt oder vorangestellt von einer rechts-nach-links-Markierung (U+200F). Dies weist s eine 200 Zeichen lange Zeichenkette zu (100 Kopien von x, durchsetzt mit 100 unsichtbaren Markierungen), wird aber unter Unicode-Regeln für allgemeinen Text als s = "x" gefolgt von einem reinen ASCII-Kommentar gerendert.
s = "x" * 100 # "x" is assigned
Die **Einbettungs-**, **Überschreibungs-** und **Isolationszeichen** für Richtungen sind ebenfalls unsichtbar, beeinflussen aber die Reihenfolge des gesamten nachfolgenden Textes, bis er entweder durch ein dediziertes Zeichen beendet wird oder bis zum Zeilenende. (Unicode legt fest, dass der Effekt bis zum Ende eines "Absatzes" andauert (siehe Unicode Bidirectional Algorithm), erlaubt aber Werkzeugen, Zeilenumbruchzeichen als Absatzenden zu interpretieren (siehe Unicode Newline Guidelines). Die meisten Code-Editoren und Terminals tun dies.)
Diese Zeichen ermöglichen im Wesentlichen eine beliebige Neuordnung des nachfolgenden Textes. Python erlaubt sie nur in Zeichenketten und Kommentaren, was ihr Potenzial einschränkt (insbesondere in Kombination mit der Tatsache, dass Python-Kommentare immer bis zum Ende einer Zeile reichen), aber sie nicht ungefährlich macht.
Normalisierung von Bezeichnern
Python-Zeichenketten sind Sammlungen von *Unicode-Codepunkten*, nicht "Zeichen".
Aus Gründen wie der Kompatibilität mit früheren Kodierungen gibt es in Unicode oft mehrere Möglichkeiten, was im Wesentlichen ein einzelnes "Zeichen" ist, zu kodieren. Zum Beispiel sind alle diese verschiedenen Wege, Å als Python-Zeichenkette zu schreiben, und jede davon ist ungleich den anderen.
"\N{LATIN CAPITAL LETTER A WITH RING ABOVE}"(1 Codepunkt)"\N{LATIN CAPITAL LETTER A}\N{COMBINING RING ABOVE}"(2 Codepunkte)"\N{ANGSTROM SIGN}"(1 Codepunkt, aber anders)
Ein weiteres Beispiel: Die Ligatur fi hat einen eigenen Unicode-Codepunkt, auch wenn sie dieselbe Bedeutung hat wie die beiden Buchstaben fi.
Außerdem haben gängige Buchstaben häufig mehrere verschiedene Variationen. Unicode bietet sie für Kontexte an, in denen der Unterschied eine semantische Bedeutung hat, wie z. B. in der Mathematik. Zum Beispiel sind einige Variationen von 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)
Unicode enthält Algorithmen zur *Normalisierung* von Varianten wie diesen in eine einzige Form, und Python-Bezeichner werden normalisiert. (Es gibt mehrere Normalformen; Python verwendet NFKC.)
Zum Beispiel sind xn und xⁿ in Python derselbe Bezeichner
>>> xⁿ = 8
>>> xn
8
… ebenso wie fi und fi, und ebenso die verschiedenen Arten, Å zu kodieren.
Diese Normalisierung gilt jedoch *nur* für Bezeichner. Funktionen, die Zeichenketten als Bezeichner behandeln, wie getattr, führen keine Normalisierung durch
>>> 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'
Dies gilt auch beim Importieren
import finalizationnormalisiert und sucht nach einer Datei namensfinalization.py(und anderenfinalization.*-Dateien).importlib.import_module("finalization")normalisiert nicht und sucht daher nach einer Datei namensfinalization.py.
Einige Dateisysteme wenden unabhängig voneinander Normalisierung und/oder Groß-/Kleinschreibung-Faltung an. Auf einigen Systemen sind finalization.py, finalization.py und FINALIZATION.py drei unterschiedliche Dateinamen; auf anderen nennen einige oder alle davon dieselbe Datei.
Quellcode-Kodierung
Die Kodierung von Python-Quellcodedateien wird durch einen spezifischen regulären Ausdruck in den ersten beiden Zeilen einer Datei gemäß Encoding-Deklarationen festgelegt. Dieser Mechanismus ist sehr liberal darin, was er akzeptiert, und daher leicht zu verschleiern.
Dies kann in Kombination mit Python-spezifischen Spezialkodierungen (siehe Text Encodings) missbraucht werden. Zum Beispiel kann mit encoding: unicode_escape, Zeichen wie Anführungszeichen oder Klammern in einer (f-)String versteckt werden, wobei viele Werkzeuge (Syntax-Hervorhebungen, Linter usw.) sie als Teil des Strings betrachten. Zum Beispiel
# 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
'''
Hier ist encoding: unicode_escape im anfänglichen Kommentar eine Kodierungserklärung. Die Kodierung unicode_escape weist Python an, \u0027 als einfaches Anführungszeichen (das einen String beginnen/beenden kann), \u002c als Komma (Trennzeichen) usw. zu behandeln.
Offene Fragen
Wir sollten wahrscheinlich schreiben und veröffentlichen
- Empfehlungen für Texteditoren und Code-Tools
- Empfehlungen für Programmierer und Teams
- Mögliche Verbesserungen in Python
Referenzen
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0672.rst
Zuletzt geändert: 2025-06-11 12:48:45 GMT