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

Python Enhancement Proposals

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

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 (wie type) 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 十= 10 eine 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 ערך = 23 wird 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 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)
  • (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 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 finalization normalisiert und sucht nach einer Datei namens finalization.py (und anderen finalization.*-Dateien).
  • importlib.import_module("finalization") normalisiert nicht und sucht daher nach einer Datei namens finalization.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

[tr36]
Unicode Technical Report #36: Unicode Security Considerations http://www.unicode.org/reports/tr36/
[tr39]
Unicode® Technical Standard #39: Unicode Security Mechanisms http://www.unicode.org/reports/tr39/
[tr55]
Unicode Technical Report #55: Unicode Source Code Handling http://www.unicode.org/reports/tr55/

Quelle: https://github.com/python/peps/blob/main/peps/pep-0672.rst

Zuletzt geändert: 2025-06-11 12:48:45 GMT