PEP 675 – Arbitrary Literal String Type
- Autor:
- Pradeep Kumar Srinivasan <gohanpra at gmail.com>, Graham Bleaney <gbleaney at gmail.com>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Typing-SIG-Thread
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 30-Nov-2021
- Python-Version:
- 3.11
- Post-History:
- 07-Feb-2022
- Resolution:
- Python-Dev Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Begründung
- Spezifikation
- Abwärtskompatibilität
- Abgelehnte Alternativen
- Referenzimplementierung
- Anhang A: Andere Verwendungen
- Anhang B: Einschränkungen
- Anhang C:
str-Methoden, dieLiteralStringbeibehalten - Anhang D: Richtlinien für die Verwendung von
LiteralStringin Stubs - Ressourcen
- Urheberrecht
Zusammenfassung
Es gibt derzeit keine Möglichkeit, mittels Typannotationen anzugeben, dass ein Funktionsparameter vom Typ eines beliebigen literalen Strings sein kann. Wir müssen einen präzisen literalen String-Typ angeben, wie z. B. Literal["foo"]. Dieses PEP führt einen Obertyp für literale String-Typen ein: LiteralString. Dies ermöglicht es einer Funktion, beliebige literale String-Typen zu akzeptieren, wie z. B. Literal["foo"] oder Literal["bar"].
Motivation
Leistungsstarke APIs, die SQL oder Shell-Befehle ausführen, empfehlen oft, dass sie mit literalen Strings aufgerufen werden, anstatt mit beliebigen benutzergesteuerten Strings. Es gibt jedoch keine Möglichkeit, diese Empfehlung im Typsystem auszudrücken, was manchmal zu Sicherheitslücken führt, wenn Entwickler sie nicht befolgen. Zum Beispiel ist eine naive Methode, einen Benutzereintrag aus einer Datenbank abzurufen, die Annahme einer Benutzer-ID und deren Einfügung in eine vordefinierte SQL-Abfrage
def query_user(conn: Connection, user_id: str) -> User:
query = f"SELECT * FROM data WHERE user_id = {user_id}"
conn.execute(query)
... # Transform data to a User object and return it
query_user(conn, "user123") # OK.
Die benutzergesteuerten Daten user_id werden jedoch mit dem SQL-Befehlsstring vermischt, was bedeutet, dass ein böswilliger Benutzer beliebige SQL-Befehle ausführen könnte
# Delete the table.
query_user(conn, "user123; DROP TABLE data;")
# Fetch all users (since 1 = 1 is always true).
query_user(conn, "user123 OR 1 = 1")
Um solche SQL-Injection-Angriffe zu verhindern, bieten SQL-APIs parametrisierte Abfragen an, die die ausgeführte Abfrage von benutzergesteuerten Daten trennen und die Ausführung beliebiger Abfragen unmöglich machen. Zum Beispiel wäre mit sqlite3 unsere ursprüngliche Funktion sicher als Abfrage mit Parametern geschrieben
def query_user(conn: Connection, user_id: str) -> User:
query = "SELECT * FROM data WHERE user_id = ?"
conn.execute(query, (user_id,))
...
Das Problem ist, dass es keine Möglichkeit gibt, diese Disziplin zu erzwingen. Die Dokumentation von sqlite3 selbst kann den Leser nur ermahnen, das sql-Argument nicht dynamisch aus externen Eingaben zu erstellen; die API-Autoren können dies nicht durch das Typsystem ausdrücken. Benutzer können immer noch eine praktische f-Zeichenkette wie zuvor verwenden (und tun dies oft) und ihren Code anfällig für SQL-Injektion lassen.
Bestehende Werkzeuge, wie der beliebte Sicherheitslinter Bandit, versuchen, unsichere externe Daten in SQL-APIs zu erkennen, indem sie die AST oder andere semantische Mustervergleiche inspizieren. Diese Werkzeuge schließen jedoch gängige Idiome aus, wie z. B. das Speichern einer großen mehrzeiligen Abfrage in einer Variablen, bevor sie ausgeführt wird, das Hinzufügen von literalen String-Modifikatoren zur Abfrage basierend auf einigen Bedingungen oder die Transformation des Abfrage-Strings mithilfe einer Funktion. (Wir untersuchen existierende Werkzeuge im Abschnitt Abgelehnte Alternativen.) Viele Werkzeuge erkennen beispielsweise ein falsch positives Problem in diesem harmlosen Ausschnitt
def query_data(conn: Connection, user_id: str, limit: bool) -> None:
query = """
SELECT
user.name,
user.age
FROM data
WHERE user_id = ?
"""
if limit:
query += " LIMIT 1"
conn.execute(query, (user_id,))
Wir möchten die schädliche Ausführung von benutzergesteuerten Daten verbieten und gleichzeitig harmlose Idiome wie das obige zulassen, ohne zusätzliche Benutzerarbeit zu verlangen.
Um dieses Ziel zu erreichen, führen wir den Typ LiteralString ein, der nur String-Werte akzeptiert, von denen bekannt ist, dass sie aus Literalen bestehen. Dies ist eine Verallgemeinerung des Typs Literal["foo"] aus PEP 586. Ein String vom Typ LiteralString kann keine benutzergesteuerten Daten enthalten. Daher ist jede API, die nur LiteralString akzeptiert, immun gegen Injektionslücken (mit pragmatischen Einschränkungen).
Da wir möchten, dass die execute-Methode von sqlite3 Strings, die mit Benutzereingaben erstellt wurden, nicht zulässt, würden wir ihren Typeshed-Stub so ändern, dass er eine sql-Abfrage vom Typ LiteralString akzeptiert
from typing import LiteralString
def execute(self, sql: LiteralString, parameters: Iterable[str] = ...) -> Cursor: ...
Dies verbietet erfolgreich unser unsicheres SQL-Beispiel. Die Variable query wird unten als Typ str inferiert, da sie aus einer Formatzeichenkette unter Verwendung von user_id erstellt wurde, und kann nicht an execute übergeben werden
def query_user(conn: Connection, user_id: str) -> User:
query = f"SELECT * FROM data WHERE user_id = {user_id}"
conn.execute(query) # Error: Expected LiteralString, got str.
...
Die Methode bleibt flexibel genug, um unser komplizierteres Beispiel zuzulassen
def query_data(conn: Connection, user_id: str, limit: bool) -> None:
# This is a literal string.
query = """
SELECT
user.name,
user.age
FROM data
WHERE user_id = ?
"""
if limit:
# Still has type LiteralString because we added a literal string.
query += " LIMIT 1"
conn.execute(query, (user_id,)) # OK
Beachten Sie, dass der Benutzer seinen SQL-Code überhaupt nicht ändern musste. Der Typ-Checker konnte den literalen String-Typ inferieren und nur bei Verstößen beanstanden.
LiteralString ist auch in anderen Fällen nützlich, in denen wir eine strenge Trennung von Befehlen und Daten wünschen, z. B. beim Erstellen von Shell-Befehlen oder beim Rendern eines Strings in eine HTML-Antwort ohne Escaping (siehe Anhang A: Andere Verwendungen). Insgesamt erleichtert diese Kombination aus Striktheit und Flexibilität die Erzwingung einer sichereren API-Nutzung in sensiblem Code, ohne die Benutzer zu belasten.
Nutzungsstatistiken
In einer Stichprobe von Open-Source-Projekten, die sqlite3 verwenden, stellten wir fest, dass conn.execute etwa 67 % der Zeit mit einem sicheren String-Literal und etwa 33 % der Zeit mit einer potenziell unsicheren lokalen String-Variable aufgerufen wurde. Die Verwendung des literalen String-Typs dieses PEPs zusammen mit einem Typ-Checker würde den unsicheren Teil dieser 33 % der Fälle (d. h. diejenigen, bei denen benutzergesteuerte Daten in die Abfrage integriert werden) verhindern, während die sicheren Fälle nahtlos zugelassen werden.
Begründung
Erstens, warum Typen verwenden, um Sicherheitslücken zu verhindern?
Benutzer in der Dokumentation zu warnen, ist nicht ausreichend - die meisten Benutzer sehen diese Warnungen entweder nie oder ignorieren sie. Die Verwendung eines bestehenden dynamischen oder statischen Analyseansatzes ist zu restriktiv - diese verhindern natürliche Idiome, wie wir im Abschnitt Motivation gesehen haben (und ausführlicher im Abschnitt Abgelehnte Alternativen diskutieren werden). Der typisierungsbasierte Ansatz in diesem PEP bietet eine benutzerfreundliche Balance zwischen Striktheit und Flexibilität.
Laufzeitansätze funktionieren nicht, weil die Abfragezeichenkette zur Laufzeit eine normale str ist. Obwohl wir einige Exploits mit Heuristiken verhindern könnten, wie z. B. Regex-Filterung auf offensichtlich böswillige Payloads, wird es immer einen Weg geben, sie zu umgehen (die perfekte Unterscheidung zwischen guten und schlechten Abfragen reduziert sich auf das Halteproblem).
Statische Ansätze, wie die Überprüfung der AST, um zu sehen, ob die Abfragezeichenkette ein literales String-Ausdruck ist, können nicht feststellen, wann ein String einer Zwischenvariable zugewiesen wird oder wann er durch eine harmlose Funktion transformiert wird. Dies macht sie übermäßig restriktiv.
Der Typ-Checker schneidet überraschenderweise besser ab als beide, da er Zugriff auf Informationen hat, die in den Laufzeit- oder statischen Analyseansätzen nicht verfügbar sind. Insbesondere kann uns der Typ-Checker sagen, ob ein Ausdruck einen literalen String-Typ hat, z. B. Literal["foo"]. Der Typ-Checker propagiert bereits Typen über Variablendeklarationen oder Funktionsaufrufe.
Im aktuellen Typsystem selbst, wenn die Funktion für die Ausführung von SQL- oder Shell-Befehlen nur drei mögliche Eingabe-Strings akzeptieren würde, wäre unsere Aufgabe erledigt. Wir würden einfach sagen
def execute(query: Literal["foo", "bar", "baz"]) -> None: ...
Aber natürlich kann execute *jede* beliebige Abfrage akzeptieren. Wie stellen wir sicher, dass die Abfrage keinen beliebigen, benutzergesteuerten String enthält?
Wir möchten angeben, dass der Wert von einem Typ Literal[<...>] sein muss, wobei <...> ein String ist. Dies repräsentiert LiteralString. LiteralString ist der „Obertyp“ aller literalen String-Typen. Tatsächlich führt dieses PEP lediglich einen Typ in der Typenhierarchie zwischen Literal["foo"] und str ein. Jeder spezifische literale String, wie Literal["foo"] oder Literal["bar"], ist mit LiteralString kompatibel, aber nicht umgekehrt. Der „Obertyp“ von LiteralString selbst ist str. Daher ist LiteralString mit str kompatibel, aber nicht umgekehrt.
Beachten Sie, dass eine Union von literalen Typen natürlich mit LiteralString kompatibel ist, da jedes Element der Union einzeln mit LiteralString kompatibel ist. Daher ist Literal["foo", "bar"] mit LiteralString kompatibel.
Wir wollen jedoch nicht nur exakte literale Abfragen darstellen. Wir wollen auch die Komposition zweier literaler Strings unterstützen, wie z. B. query + " LIMIT 1". Dies ist mit dem obigen Konzept ebenfalls möglich. Wenn x und y zwei Werte vom Typ LiteralString sind, dann ist x + y ebenfalls von einem Typ, der mit LiteralString kompatibel ist. Wir können dies durch Betrachtung spezifischer Instanzen wie Literal["foo"] und Literal["bar"] nachvollziehen; der Wert des addierten Strings x + y kann nur "foobar" sein, was den Typ Literal["foobar"] hat und somit mit LiteralString kompatibel ist. Die gleiche Überlegung gilt, wenn x und y Unions von literalen Typen sind; das Ergebnis der paarweisen Addition zweier literaler Typen aus x bzw. y ist ein literaler Typ, was bedeutet, dass das Gesamtergebnis eine Union von literalen Typen ist und somit mit LiteralString kompatibel ist.
Auf diese Weise können wir Pythons Konzept eines Literal-String-Typs nutzen, um anzugeben, dass unsere API nur Strings akzeptieren kann, von denen bekannt ist, dass sie aus Literalen konstruiert sind. Spezifischere Details folgen in den restlichen Abschnitten.
Spezifikation
Laufzeitverhalten
Wir schlagen vor, LiteralString zu typing.py hinzuzufügen, mit einer Implementierung, die typing.NoReturn ähnelt.
Beachten Sie, dass LiteralString eine spezielle Form ist, die ausschließlich zur Typüberprüfung verwendet wird. Es gibt keinen Ausdruck, für den type(<expr>) zur Laufzeit LiteralString ergibt. Daher geben wir in der Implementierung nicht an, dass es eine Unterklasse von str ist.
Gültige Orte für LiteralString
LiteralString kann dort verwendet werden, wo jeder andere Typ verwendet werden kann
variable_annotation: LiteralString
def my_function(literal_string: LiteralString) -> LiteralString: ...
class Foo:
my_attribute: LiteralString
type_argument: List[LiteralString]
T = TypeVar("T", bound=LiteralString)
Es kann nicht in Unions von Literal-Typen verschachtelt werden
bad_union: Literal["hello", LiteralString] # Not OK
bad_nesting: Literal[LiteralString] # Not OK
Typinferenz
Inferenz von LiteralString
Jeder literale String-Typ ist mit LiteralString kompatibel. Zum Beispiel ist x: LiteralString = "foo" gültig, weil "foo" als Typ Literal["foo"] inferiert wird.
Wie in der Begründung schlussfolgern wir LiteralString auch in den folgenden Fällen
- Addition:
x + yist vom TypLiteralString, wenn sowohlxals auchymitLiteralStringkompatibel sind. - Verknüpfen:
sep.join(xs)ist vom TypLiteralString, wenn der Typ vonsepmitLiteralStringkompatibel ist und der Typ vonxsmitIterable[LiteralString]kompatibel ist. - In-Place-Addition: Wenn
sden TypLiteralStringhat undxeinen mitLiteralStringkompatiblen Typ hat, dann behälts += xden Typ vonsalsLiteralStringbei. - String-Formatierung: Eine f-Zeichenkette hat den Typ
LiteralString, wenn und nur wenn ihre Bestandteile literale Strings sind.s.format(...)hat den TypLiteralString, wenn und nur wennsund die Argumente Typen haben, die mitLiteralStringkompatibel sind. - Literale-erhaltende Methoden: In Anhang C haben wir eine vollständige Liste von
str-Methoden bereitgestellt, die den TypLiteralStringbeibehalten.
In allen anderen Fällen, wenn einer oder mehrere der komponierten Werte einen nicht-literalen Typ str haben, hat die Komposition der Typen den Typ str. Zum Beispiel, wenn s den Typ str hat, dann hat "hello" + s den Typ str. Dies entspricht dem bereits bestehenden Verhalten von Typ-Checkern.
LiteralString ist mit dem Typ str kompatibel. Es erbt alle Methoden von str. Wenn wir also eine Variable s vom Typ LiteralString haben, ist es sicher zu schreiben s.startswith("hello").
Einige Typ-Checker verfeinern den Typ eines Strings bei einer Gleichheitsprüfung
def foo(s: str) -> None:
if s == "bar":
reveal_type(s) # => Literal["bar"]
Ein derart verfeinerter Typ im if-Block ist auch mit LiteralString kompatibel, da sein Typ Literal["bar"] ist.
Beispiele
Siehe die folgenden Beispiele zur Verdeutlichung der obigen Regeln
literal_string: LiteralString
s: str = literal_string # OK
literal_string: LiteralString = s # Error: Expected LiteralString, got str.
literal_string: LiteralString = "hello" # OK
Addition von literalen Strings
def expect_literal_string(s: LiteralString) -> None: ...
expect_literal_string("foo" + "bar") # OK
expect_literal_string(literal_string + "bar") # OK
literal_string2: LiteralString
expect_literal_string(literal_string + literal_string2) # OK
plain_string: str
expect_literal_string(literal_string + plain_string) # Not OK.
Verknüpfung mit literalen Strings
expect_literal_string(",".join(["foo", "bar"])) # OK
expect_literal_string(literal_string.join(["foo", "bar"])) # OK
expect_literal_string(literal_string.join([literal_string, literal_string2])) # OK
xs: List[LiteralString]
expect_literal_string(literal_string.join(xs)) # OK
expect_literal_string(plain_string.join([literal_string, literal_string2]))
# Not OK because the separator has type 'str'.
In-Place-Addition mit literalen Strings
literal_string += "foo" # OK
literal_string += literal_string2 # OK
literal_string += plain_string # Not OK
Formatierungsstrings mit literalen Strings
literal_name: LiteralString
expect_literal_string(f"hello {literal_name}")
# OK because it is composed from literal strings.
expect_literal_string("hello {}".format(literal_name)) # OK
expect_literal_string(f"hello") # OK
username: str
expect_literal_string(f"hello {username}")
# NOT OK. The format-string is constructed from 'username',
# which has type 'str'.
expect_literal_string("hello {}".format(username)) # Not OK
Andere literale Typen, wie z. B. literale Integer, sind nicht mit LiteralString kompatibel
some_int: int
expect_literal_string(some_int) # Error: Expected LiteralString, got int.
literal_one: Literal[1] = 1
expect_literal_string(literal_one) # Error: Expected LiteralString, got Literal[1].
Wir können Funktionen auf literalen Strings aufrufen
def add_limit(query: LiteralString) -> LiteralString:
return query + " LIMIT = 1"
def my_query(query: LiteralString, user_id: str) -> None:
sql_connection().execute(add_limit(query), (user_id,)) # OK
Bedingte Anweisungen und Ausdrücke funktionieren wie erwartet
def return_literal_string() -> LiteralString:
return "foo" if condition1() else "bar" # OK
def return_literal_str2(literal_string: LiteralString) -> LiteralString:
return "foo" if condition1() else literal_string # OK
def return_literal_str3() -> LiteralString:
if condition1():
result: Literal["foo"] = "foo"
else:
result: LiteralString = "bar"
return result # OK
Interaktion mit Typvariablen und Generika
Typvariablen können an LiteralString gebunden werden
from typing import Literal, LiteralString, TypeVar
TLiteral = TypeVar("TLiteral", bound=LiteralString)
def literal_identity(s: TLiteral) -> TLiteral:
return s
hello: Literal["hello"] = "hello"
y = literal_identity(hello)
reveal_type(y) # => Literal["hello"]
s: LiteralString
y2 = literal_identity(s)
reveal_type(y2) # => LiteralString
s_error: str
literal_identity(s_error)
# Error: Expected TLiteral (bound to LiteralString), got str.
LiteralString kann als Typargument für generische Klassen verwendet werden
class Container(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
literal_string: LiteralString = "hello"
x: Container[LiteralString] = Container(literal_string) # OK
s: str
x_error: Container[LiteralString] = Container(s) # Not OK
Standardcontainer wie List funktionieren wie erwartet
xs: List[LiteralString] = ["foo", "bar", "baz"]
Interaktionen mit Überladungen
Literale Strings und Überladungen müssen nicht auf besondere Weise interagieren: Die bestehenden Regeln funktionieren gut. LiteralString kann als Fallback-Überladung verwendet werden, wenn ein bestimmter Literal["foo"]-Typ nicht übereinstimmt
@overload
def foo(x: Literal["foo"]) -> int: ...
@overload
def foo(x: LiteralString) -> bool: ...
@overload
def foo(x: str) -> str: ...
x1: int = foo("foo") # First overload.
x2: bool = foo("bar") # Second overload.
s: str
x3: str = foo(s) # Third overload.
Abwärtskompatibilität
Wir schlagen vor, typing_extensions.LiteralString für die Verwendung in früheren Python-Versionen hinzuzufügen.
Wie in PEP 586 erwähnt, können Typ-Checker „frei experimentieren, um ausgefeiltere Inferenztechniken zu verwenden“. Wenn der Typ-Checker also einen literalen String-Typ für eine nicht annotierte Variable inferiert, die mit einem literalen String initialisiert wird, sollte das folgende Beispiel in Ordnung sein
x = "hello"
expect_literal_string(x)
# OK, because x is inferred to have type 'Literal["hello"]'.
Dies ermöglicht eine präzise Typüberprüfung von idiomatischem SQL-Abfragecode, ohne den Code überhaupt annotieren zu müssen (wie im Beispiel im Abschnitt Motivation zu sehen ist).
Dieses PEP schreibt die obige Inferenzstrategie jedoch nicht vor, ebenso wie PEP 586. Falls der Typ-Checker x nicht als Typ Literal["hello"] inferiert, können Benutzer den Typ-Checker unterstützen, indem sie ihn explizit als x: LiteralString annotieren
x: LiteralString = "hello"
expect_literal_string(x)
Abgelehnte Alternativen
Warum nicht Werkzeug X verwenden?
Werkzeuge zur Erkennung von Problemen wie SQL-Injection gibt es in drei Varianten: AST-basiert, Funktionsanalysen und Taint-Flow-Analysen.
AST-basierte Werkzeuge: Bandit hat ein Plugin, das warnt, wenn SQL-Abfragen keine literalen Strings sind. Das Problem ist, dass viele absolut sichere SQL-Abfragen dynamisch aus String-Literalen aufgebaut werden, wie im Abschnitt Motivation gezeigt. Auf AST-Ebene erscheint die resultierende SQL-Abfrage nicht mehr als String-Literal und ist somit von einem potenziell bösartigen String nicht zu unterscheiden. Die Verwendung dieser Werkzeuge würde die Fähigkeit der Entwickler, SQL-Abfragen zu erstellen, erheblich einschränken. LiteralString kann ähnliche Sicherheitsgarantien mit weniger Einschränkungen bieten.
Semgrep und pyanalyze: Semgrep unterstützt eine fortschrittlichere Funktionsanalysen, einschließlich Konstantenpropagierung innerhalb einer Funktion. Dies ermöglicht es uns, Injection-Angriffe zu verhindern und gleichzeitig einige Formen sicherer dynamischer SQL-Abfragen innerhalb einer Funktion zuzulassen. pyanalyze hat eine ähnliche Erweiterung. Aber weder verarbeitet Funktionsaufrufe, die sichere SQL-Abfragen erstellen und zurückgeben. Zum Beispiel, im folgenden Code-Beispiel, build_insert_query ist eine Hilfsfunktion zum Erstellen einer Abfrage, die mehrere Werte in die entsprechenden Spalten einfügt. Semgrep und pyanalyze verbieten diese natürliche Verwendung, während LiteralString sie ohne Belastung für den Programmierer behandelt
def build_insert_query(
table: LiteralString
insert_columns: Iterable[LiteralString],
) -> LiteralString:
sql = "INSERT INTO " + table
column_clause = ", ".join(insert_columns)
value_clause = ", ".join(["?"] * len(insert_columns))
sql += f" ({column_clause}) VALUES ({value_clause})"
return sql
def insert_data(
conn: Connection,
kvs_to_insert: Dict[LiteralString, str]
) -> None:
query = build_insert_query("data", kvs_to_insert.keys())
conn.execute(query, kvs_to_insert.values())
# Example usage
data_to_insert = {
"column_1": value_1, # Note: values are not literals
"column_2": value_2,
"column_3": value_3,
}
insert_data(conn, data_to_insert)
Taint-Flow-Analyse: Werkzeuge wie Pysa oder CodeQL können Daten von einer benutzergesteuerten Eingabe in eine SQL-Abfrage verfolgen. Diese Werkzeuge sind leistungsstark, aber mit erheblichem Aufwand verbunden, um das Werkzeug in CI einzurichten, „Taint“-Senken und -Quellen zu definieren und Entwicklern die Nutzung beizubringen. Sie dauern auch in der Regel länger als ein Typ-Checker (Minuten statt Sekunden), was bedeutet, dass das Feedback nicht sofort erfolgt. Schließlich verlagern sie die Last der Verhinderung von Schwachstellen auf die Bibliotheksbenutzer, anstatt es den Bibliotheken selbst zu ermöglichen, präzise anzugeben, wie ihre APIs aufgerufen werden müssen (wie es mit LiteralString möglich ist).
Ein letzter Grund, die Verwendung eines neuen Typs einem dedizierten Werkzeug vorzuziehen, ist, dass Typ-Checker weiter verbreitet sind als dedizierte Sicherheitstools; zum Beispiel wurde MyPy im Januar 2022 über 7 Millionen Mal heruntergeladen, im Vergleich zu weniger als 2 Millionen Mal für Bandit. Sicherheitsschutzmaßnahmen, die direkt in Typ-Checker integriert sind, bedeuten, dass mehr Entwickler davon profitieren.
Warum nicht ein NewType für str verwenden?
Jede API, für die LiteralString geeignet wäre, könnte stattdessen aktualisiert werden, um einen anderen Typ zu akzeptieren, der innerhalb des Python-Typsystems erstellt wurde, z. B. NewType("SafeSQL", str)
SafeSQL = NewType("SafeSQL", str)
def execute(self, sql: SafeSQL, parameters: Iterable[str] = ...) -> Cursor: ...
execute(SafeSQL("SELECT * FROM data WHERE user_id = ?"), user_id) # OK
user_query: str
execute(user_query) # Error: Expected SafeSQL, got str.
Die Notwendigkeit, einen neuen Typ zu erstellen, um eine API aufzurufen, könnte einige Entwickler zum Nachdenken anregen und zu mehr Vorsicht ermutigen, aber es garantiert nicht, dass Entwickler nicht einfach einen benutzergesteuerten String in den neuen Typ umwandeln und ihn trotzdem an die modifizierte API übergeben
query = f"SELECT * FROM data WHERE user_id = f{user_id}"
execute(SafeSQL(query)) # No error!
Damit sind wir wieder am Anfang des Problems, die Annahme beliebiger Eingaben in SafeSQL zu verhindern. Dies ist auch kein theoretisches Problem. Django verwendet den obigen Ansatz mit SafeString und mark_safe. Probleme wie CVE-2020-13596 zeigen, wie diese Technik versagen kann.
Beachten Sie auch, dass dies invasive Änderungen am Quellcode erfordert (Einschließen der Abfrage mit SafeSQL), während LiteralString keine solchen Änderungen erfordert. Benutzer können sich davon unbeeindruckt zeigen, solange sie literale Strings an sensible APIs übergeben.
Warum nicht versuchen, vertrauenswürdige Typen zu emulieren?
Trusted Types ist eine W3C-Spezifikation zur Verhinderung von DOM-basiertem Cross-Site Scripting (XSS). XSS tritt auf, wenn gefährliche Browser-APIs rohe, benutzergesteuerte Strings akzeptieren. Die Spezifikation modifiziert diese APIs so, dass sie nur die „Trusted Types“ akzeptieren, die von bestimmten Sanierungsfunktionen zurückgegeben werden. Diese Sanierungsfunktionen müssen einen potenziell bösartigen String entgegennehmen und ihn validieren oder auf andere Weise harmlos machen, z. B. indem sie überprüfen, ob es sich um eine gültige URL handelt oder sie HTML-kodieren.
Es kann verlockend sein anzunehmen, dass die Übertragung des Konzepts von Trusted Types auf Python das Problem lösen könnte. Der grundlegende Unterschied ist jedoch, dass die Ausgabe eines Trusted Types-Sanierers normalerweise *nicht ausführbarer Code* sein soll. Daher ist es einfach, die Eingabe HTML-zu-kodieren, gefährliche Tags zu entfernen oder sie anderweitig inert zu machen. Bei einer SQL-Abfrage oder einem Shell-Befehl muss das Endergebnis *immer noch ausführbarer Code* sein. Es gibt keine Möglichkeit, einen Sanierer zu schreiben, der zuverlässig feststellen kann, welche Teile eines Eingabe-Strings harmlos und welche potenziell bösartig sind.
Laufzeitprüfbare LiteralString
Das LiteralString-Konzept könnte über die statische Typüberprüfung hinaus erweitert werden, um eine zur Laufzeit prüfbare Eigenschaft von str-Objekten zu sein. Dies würde einige Vorteile bieten, z. B. die Möglichkeit für Frameworks, Fehler bei dynamischen Strings auszulösen. Solche Laufzeitfehler wären ein robusterer Abwehrmechanismus als Typfehler, die potenziell unterdrückt, ignoriert oder nie gesehen werden können, wenn der Autor keinen Typ-Checker verwendet.
Diese Erweiterung des LiteralString-Konzepts würde den Umfang des Vorschlags dramatisch erhöhen, da Änderungen an einem der grundlegendsten Typen in Python erforderlich wären. Obwohl Laufzeit-Taint-Prüfungen für Strings, ähnlich wie bei Perls taint, erwägt und versucht wurde, und andere sie in Zukunft in Betracht ziehen mögen, fallen solche Erweiterungen nicht in den Rahmen dieses PEPs.
Abgelehnte Namen
Wir haben eine Vielzahl von Namen für den literalen String-Typ in Betracht gezogen und Ideen auf typing-sig eingeholt. Einige bemerkenswerte Alternativen waren
Literal[str]: Dies ist eine natürliche Erweiterung des Namens des TypsLiteral["foo"], aber typing-sig widersprach, dass Benutzer dies mit dem literalen Typ derstr-Klasse verwechseln könnten.LiteralStr: Dies ist kürzer alsLiteralString, sieht aber für die PEP-Autoren seltsam aus.LiteralDerivedString: Dies (zusammen mitMadeFromLiteralString) erfasst die technische Bedeutung des Typs am besten. Es repräsentiert nicht nur den Typ von literalen Ausdrücken, wie z. B."foo", sondern auch den von aus Literalen komponierten Ausdrücken, wie z. B."foo" + "bar". Beide Namen scheinen jedoch umständlich zu sein.StringLiteral: Benutzer könnten dies mit dem bestehenden Konzept von „String-Literalen“ verwechseln, bei dem der String als syntaktisches Token im Quellcode existiert, während unser Konzept allgemeiner ist.SafeString: Obwohl dies unserer beabsichtigten Bedeutung nahekommt, könnte es Benutzer irreführen, dass der String in irgendeiner Weise bereinigt wurde, vielleicht durch Escaping von HTML-Tags oder Shell-bezogenen Sonderzeichen.ConstantStr: Dies erfasst nicht die Idee der Komposition von literalen Strings.StaticStr: Dies deutet darauf hin, dass der String statisch berechenbar ist, d. h. ohne Ausführung des Programms berechenbar, was nicht der Fall ist. Der literale String kann je nach Laufzeit-Flags variieren, wie in den Beispielen der Motivation gezeigt.LiteralOnly[str]: Dies hat den Vorteil, dass es auf andere literale Typen wiebytesoderinterweitert werden kann. Wir fanden jedoch die Erweiterbarkeit nicht lohnenswert gegenüber dem Verlust an Lesbarkeit.
Insgesamt gab es über einen langen Zeitraum keinen klaren Gewinner auf typing-sig, daher beschlossen wir, die Waage zugunsten von LiteralString zu kippen.
LiteralBytes
Wir könnten literale Byte-Typen, wie z. B. Literal[b"foo"], zu LiteralBytes verallgemeinern. Literale Byte-Typen werden jedoch viel seltener verwendet als literale String-Typen, und wir fanden keine große Nachfrage von Benutzern nach LiteralBytes, daher haben wir beschlossen, sie nicht in dieses PEP aufzunehmen. Andere könnten sie jedoch in zukünftigen PEPs in Betracht ziehen.
Referenzimplementierung
Dies ist in Pyre v0.9.8 implementiert und wird aktiv genutzt.
Die Implementierung erweitert den Typüberprüfer einfach um LiteralString als Obertyp von literalen String-Typen.
Um die Komposition durch Addition, Join usw. zu unterstützen, war es ausreichend, die Stubs für str in Pyres Kopie von typeshed zu überladen.
Anhang A: Andere Verwendungen
Um die Diskussion zu vereinfachen und minimale Sicherheitskenntnisse vorauszusetzen, haben wir uns im gesamten PEP auf SQL-Injections konzentriert. LiteralString kann jedoch auch verwendet werden, um viele andere Arten von Injection-Schwachstellen zu verhindern.
Befehlsinjektion
APIs wie subprocess.run akzeptieren einen String, der als Shell-Befehl ausgeführt werden kann
subprocess.run(f"echo 'Hello {name}'", shell=True)
Wenn benutzergesteuerte Daten in den Befehlsstring einbezogen werden, ist der Code anfällig für "Command Injection"; d.h. ein Angreifer kann bösartige Befehle ausführen. Beispielsweise würde ein Wert von ' && rm -rf / # dazu führen, dass der folgende zerstörerische Befehl ausgeführt wird
echo 'Hello ' && rm -rf / #'
Diese Schwachstelle könnte verhindert werden, indem run so aktualisiert wird, dass es nur LiteralString akzeptiert, wenn es im Modus shell=True verwendet wird. Hier ist ein vereinfachter Stub
def run(command: LiteralString, *args: str, shell: bool=...): ...
Cross-Site Scripting (XSS)
Die meisten populären Python-Webframeworks, wie z. B. Django, verwenden eine Template-Engine, um HTML aus Benutzerdaten zu generieren. Diese Template-Sprachen escapen Benutzerdaten automatisch, bevor sie in das HTML-Template eingefügt werden, und verhindern somit Cross-Site-Scripting (XSS)-Schwachstellen.
Ein gängiger Weg, Auto-Escaping zu umgehen und HTML unverändert zu rendern, ist die Verwendung von Funktionen wie mark_safe in Django oder do_mark_safe in Jinja2, was XSS-Schwachstellen verursacht.
dangerous_string = django.utils.safestring.mark_safe(f"<script>{user_input}</script>")
return(dangerous_string)
Diese Schwachstelle könnte verhindert werden, indem mark_safe so aktualisiert wird, dass es nur LiteralString akzeptiert.
def mark_safe(s: LiteralString) -> str: ...
Server-Side Template Injection (SSTI)
Templating-Frameworks wie Jinja erlauben Python-Ausdrücke, die ausgewertet und in das gerenderte Ergebnis eingefügt werden.
template_str = "There are {{ len(values) }} values: {{ values }}"
template = jinja2.Template(template_str)
template.render(values=[1, 2])
# Result: "There are 2 values: [1, 2]"
Wenn ein Angreifer die Vorlagenzeichenfolge ganz oder teilweise kontrolliert, kann er Ausdrücke einfügen, die beliebigen Code ausführen und die Anwendung kompromittieren.
malicious_str = "{{''.__class__.__base__.__subclasses__()[408]('rm - rf /',shell=True)}}"
template = jinja2.Template(malicious_str)
template.render()
# Result: The shell command 'rm - rf /' is run
Exploits von Template-Injection wie diesem könnten verhindert werden, indem die Template API so aktualisiert wird, dass sie nur LiteralString akzeptiert.
class Template:
def __init__(self, source: LiteralString): ...
Logging-Format-String-Injektion
Logging-Frameworks erlauben oft, dass ihre Eingabezeichenfolgen Formatierungsdirektiven enthalten. Im schlimmsten Fall hat die Erlaubnis für Benutzer, die Protokollzeichenfolge zu kontrollieren, zu CVE-2021-44228 (umgangssprachlich bekannt als log4shell) geführt, die als die "kritischste Schwachstelle des letzten Jahrzehnts" beschrieben wurde. Obwohl derzeit keine Python-Frameworks für einen ähnlichen Angriff anfällig sind, bietet das eingebaute Logging-Framework Formatierungsoptionen, die anfällig für Denial-of-Service-Angriffe durch extern kontrollierte Logging-Strings sind. Das folgende Beispiel veranschaulicht ein einfaches Denial-of-Service-Szenario
external_string = "%(foo)999999999s"
...
# Tries to add > 1GB of whitespace to the logged string:
logger.info(f'Received: {external_string}', some_dict)
Diese Art von Angriff könnte verhindert werden, indem gefordert wird, dass die an den Logger übergebene Formatzeichenfolge ein LiteralString ist und alle extern kontrollierten Daten separat als Argumente übergeben werden (wie in Issue 46200 vorgeschlagen).
def info(msg: LiteralString, *args: object) -> None:
...
Anhang B: Einschränkungen
Es gibt eine Reihe von Möglichkeiten, wie LiteralString Benutzer daran hindern könnte, Strings, die aus Nicht-Literal-Daten erstellt wurden, an eine API zu übergeben.
1. Wenn der Entwickler keinen Typüberprüfer verwendet oder keine Typannotationen hinzufügt, werden Verstöße nicht erkannt.
2. cast(LiteralString, non_literal_string) könnte verwendet werden, um dem Typüberprüfer eine falsche Information zu geben und einen dynamischen String-Wert als LiteralString auszugeben. Das Gleiche gilt für eine Variable vom Typ Any.
3. Kommentare wie # type: ignore könnten verwendet werden, um Warnungen über Nicht-Literal-Strings zu ignorieren.
4. Triviale Funktionen könnten konstruiert werden, um einen str in einen LiteralString zu konvertieren.
def make_literal(s: str) -> LiteralString:
letters: Dict[str, LiteralString] = {
"A": "A",
"B": "B",
...
}
output: List[LiteralString] = [letters[c] for c in s]
return "".join(output)
Wir könnten die oben genannten Punkte durch Linting, Code-Reviews usw. abmildern, aber letztendlich wird ein cleverer, böswilliger Entwickler, der versucht, die von LiteralString angebotenen Schutzmechanismen zu umgehen, immer erfolgreich sein. Wichtig ist zu bedenken, dass LiteralString nicht dazu gedacht ist, vor *bösartigen* Entwicklern zu schützen; es soll vor gutmütigen Entwicklern schützen, die versehentlich sensible APIs auf gefährliche Weise verwenden (ohne sie anderweitig zu behindern).
Ohne LiteralString ist das beste Werkzeug zur Durchsetzung, das API-Autoren zur Verfügung steht, die Dokumentation, die leicht ignoriert und oft nicht gelesen wird. Mit LiteralString erfordert API-Missbrauch bewusste Gedanken und Artefakte im Code, die Prüfer und zukünftige Entwickler bemerken können.
Anhang C: str-Methoden, die LiteralString beibehalten
Die str-Klasse hat mehrere Methoden, die von LiteralString profitieren würden. Zum Beispiel könnten Benutzer erwarten, dass "hello".capitalize() den Typ LiteralString hat, ähnlich wie die anderen Beispiele, die wir im Abschnitt Inferring LiteralString gesehen haben. Die Inferenz des Typs LiteralString ist korrekt, da der String kein beliebiger, vom Benutzer bereitgestellter String ist – wir wissen, dass er den Typ Literal["HELLO"] hat, der mit LiteralString kompatibel ist. Mit anderen Worten, die Methode capitalize erhält den Typ LiteralString. Es gibt mehrere andere str-Methoden, die LiteralString erhalten.
Wir schlagen vor, den Stub für str in typeshed zu aktualisieren, damit die Methoden mit den LiteralString-erhaltenden Versionen überladen werden. Das bedeutet, dass Typüberprüfer das LiteralString-Verhalten für jede Methode nicht hartkodieren müssen. Außerdem können wir so zukünftige Methoden einfach durch Aktualisieren des typeshed-Stubs unterstützen.
Zum Beispiel würden wir zum Erhalten von literalen Typen für die capitalize-Methode den Stub wie folgt ändern
# before
def capitalize(self) -> str: ...
# after
@overload
def capitalize(self: LiteralString) -> LiteralString: ...
@overload
def capitalize(self) -> str: ...
Der Nachteil der Änderung des str-Stubs ist, dass der Stub komplizierter wird und Fehlermeldungen schwerer verständlich machen kann. Typüberprüfer müssen str möglicherweise speziell behandeln, um Fehlermeldungen für Benutzer verständlich zu machen.
Unten finden Sie eine vollständige Liste von str-Methoden, die, wenn sie mit Argumenten vom Typ LiteralString aufgerufen werden, als Rückgabe eines LiteralString behandelt werden müssen. Wenn dieser PEP akzeptiert wird, werden wir diese Methodensignaturen in typeshed aktualisieren.
@overload
def capitalize(self: LiteralString) -> LiteralString: ...
@overload
def capitalize(self) -> str: ...
@overload
def casefold(self: LiteralString) -> LiteralString: ...
@overload
def casefold(self) -> str: ...
@overload
def center(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def center(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...
if sys.version_info >= (3, 8):
@overload
def expandtabs(self: LiteralString, tabsize: SupportsIndex = ...) -> LiteralString: ...
@overload
def expandtabs(self, tabsize: SupportsIndex = ...) -> str: ...
else:
@overload
def expandtabs(self: LiteralString, tabsize: int = ...) -> LiteralString: ...
@overload
def expandtabs(self, tabsize: int = ...) -> str: ...
@overload
def format(self: LiteralString, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ...
@overload
def format(self, *args: str, **kwargs: str) -> str: ...
@overload
def join(self: LiteralString, __iterable: Iterable[LiteralString]) -> LiteralString: ...
@overload
def join(self, __iterable: Iterable[str]) -> str: ...
@overload
def ljust(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def ljust(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...
@overload
def lower(self: LiteralString) -> LiteralString: ...
@overload
def lower(self) -> LiteralString: ...
@overload
def lstrip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def lstrip(self, __chars: str | None = ...) -> str: ...
@overload
def partition(self: LiteralString, __sep: LiteralString) -> tuple[LiteralString, LiteralString, LiteralString]: ...
@overload
def partition(self, __sep: str) -> tuple[str, str, str]: ...
@overload
def replace(self: LiteralString, __old: LiteralString, __new: LiteralString, __count: SupportsIndex = ...) -> LiteralString: ...
@overload
def replace(self, __old: str, __new: str, __count: SupportsIndex = ...) -> str: ...
if sys.version_info >= (3, 9):
@overload
def removeprefix(self: LiteralString, __prefix: LiteralString) -> LiteralString: ...
@overload
def removeprefix(self, __prefix: str) -> str: ...
@overload
def removesuffix(self: LiteralString, __suffix: LiteralString) -> LiteralString: ...
@overload
def removesuffix(self, __suffix: str) -> str: ...
@overload
def rjust(self: LiteralString, __width: SupportsIndex, __fillchar: LiteralString = ...) -> LiteralString: ...
@overload
def rjust(self, __width: SupportsIndex, __fillchar: str = ...) -> str: ...
@overload
def rpartition(self: LiteralString, __sep: LiteralString) -> tuple[LiteralString, LiteralString, LiteralString]: ...
@overload
def rpartition(self, __sep: str) -> tuple[str, str, str]: ...
@overload
def rsplit(self: LiteralString, sep: LiteralString | None = ..., maxsplit: SupportsIndex = ...) -> list[LiteralString]: ...
@overload
def rsplit(self, sep: str | None = ..., maxsplit: SupportsIndex = ...) -> list[str]: ...
@overload
def rstrip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def rstrip(self, __chars: str | None = ...) -> str: ...
@overload
def split(self: LiteralString, sep: LiteralString | None = ..., maxsplit: SupportsIndex = ...) -> list[LiteralString]: ...
@overload
def split(self, sep: str | None = ..., maxsplit: SupportsIndex = ...) -> list[str]: ...
@overload
def splitlines(self: LiteralString, keepends: bool = ...) -> list[LiteralString]: ...
@overload
def splitlines(self, keepends: bool = ...) -> list[str]: ...
@overload
def strip(self: LiteralString, __chars: LiteralString | None = ...) -> LiteralString: ...
@overload
def strip(self, __chars: str | None = ...) -> str: ...
@overload
def swapcase(self: LiteralString) -> LiteralString: ...
@overload
def swapcase(self) -> str: ...
@overload
def title(self: LiteralString) -> LiteralString: ...
@overload
def title(self) -> str: ...
@overload
def upper(self: LiteralString) -> LiteralString: ...
@overload
def upper(self) -> str: ...
@overload
def zfill(self: LiteralString, __width: SupportsIndex) -> LiteralString: ...
@overload
def zfill(self, __width: SupportsIndex) -> str: ...
@overload
def __add__(self: LiteralString, __s: LiteralString) -> LiteralString: ...
@overload
def __add__(self, __s: str) -> str: ...
@overload
def __iter__(self: LiteralString) -> Iterator[str]: ...
@overload
def __iter__(self) -> Iterator[str]: ...
@overload
def __mod__(self: LiteralString, __x: Union[LiteralString, Tuple[LiteralString, ...]]) -> str: ...
@overload
def __mod__(self, __x: Union[str, Tuple[str, ...]]) -> str: ...
@overload
def __mul__(self: LiteralString, __n: SupportsIndex) -> LiteralString: ...
@overload
def __mul__(self, __n: SupportsIndex) -> str: ...
@overload
def __repr__(self: LiteralString) -> LiteralString: ...
@overload
def __repr__(self) -> str: ...
@overload
def __rmul__(self: LiteralString, n: SupportsIndex) -> LiteralString: ...
@overload
def __rmul__(self, n: SupportsIndex) -> str: ...
@overload
def __str__(self: LiteralString) -> LiteralString: ...
@overload
def __str__(self) -> str: ...
Anhang D: Richtlinien für die Verwendung von LiteralString in Stubs
Bibliotheken, die keine Typannotationen in ihrem Quellcode enthalten, können Typstubs in Typeshed angeben. Bibliotheken, die in anderen Sprachen geschrieben sind, wie z.B. für maschinelles Lernen, können ebenfalls Python-Typstubs bereitstellen. Das bedeutet, dass der Typüberprüfer nicht verifizieren kann, dass die Typannotationen mit dem Quellcode übereinstimmen, und dem Typstub vertrauen muss. Daher müssen Autoren von Typstubs vorsichtig sein, wenn sie LiteralString verwenden, da eine Funktion fälschlicherweise als sicher erscheinen kann, obwohl sie es nicht ist.
Wir empfehlen die folgenden Richtlinien für die Verwendung von LiteralString in Stubs
- Wenn der Stub für eine reine Funktion ist, empfehlen wir die Verwendung von
LiteralStringim Rückgabetyp der Funktion oder ihrer Überladungen nur dann, wenn alle entsprechenden Parameter literale Typen haben (d.h.LiteralStringoderLiteral["a", "b"]).# OK @overload def my_transform(x: LiteralString, y: Literal["a", "b"]) -> LiteralString: ... @overload def my_transform(x: str, y: str) -> str: ... # Not OK @overload def my_transform(x: LiteralString, y: str) -> LiteralString: ... @overload def my_transform(x: str, y: str) -> str: ...
- Wenn der Stub für eine
staticmethodist, empfehlen wir die gleiche Richtlinie wie oben. - Wenn der Stub für eine andere Art von Methode ist, raten wir von der Verwendung von
LiteralStringim Rückgabetyp der Methode oder einer ihrer Überladungen ab. Dies liegt daran, dass auch wenn alle expliziten Parameter den TypLiteralStringhaben, das Objekt selbst möglicherweise unter Verwendung von Benutzerdaten erstellt wurde und somit der Rückgabetyp benutzergesteuert sein kann. - Wenn der Stub für ein Klassenattribut oder eine globale Variable ist, raten wir ebenfalls von der Verwendung von
LiteralStringab, da der untypisierte Code beliebige Werte in das Attribut schreiben kann.
Wir überlassen die endgültige Entscheidung jedoch dem Autor der Bibliothek. Er kann LiteralString verwenden, wenn er zuversichtlich ist, dass der von der Methode oder Funktion zurückgegebene String oder der im Attribut gespeicherte String garantiert einen literalen Typ hat – d.h. der String wird durch Anwenden von nur literale-erhaltenden str-Operationen auf einen String-Literal erstellt.
Beachten Sie, dass diese Richtlinien nicht für Inline-Typannotationen gelten, da der Typüberprüfer verifizieren kann, dass beispielsweise eine Methode, die LiteralString zurückgibt, tatsächlich einen Ausdruck dieses Typs zurückgibt.
Ressourcen
Literale String-Typen in Scala
Scala verwendet Singleton als Obertyp für Singleton-Typen, was literale String-Typen wie "foo" einschließt. Singleton ist Scalas verallgemeinertes Analogon des LiteralString dieses PEPs.
Tamer Abdulradi zeigte, wie Scalas literale String-Typen für "Preventing SQL injection at compile time" verwendet werden können, Scala Days Vortrag Literal types: What are they good for? (Folien 52 bis 68).
Danksagungen
Dank der folgenden Personen für ihr Feedback zur PEP.
Edward Qiu, Jia Chen, Shannon Zhu, Gregory P. Smith, Никита Соболев, CAM Gerlach, Arie Bovenberg, David Foster und Shengye Wan
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-0675.rst
Zuletzt geändert: 2024-06-11 22:12:09 GMT