Aufwärmen · Aufgabe 1 von 7
Aufwärmen aus der vorigen Lektion: Die Eingabe ist an einen Platzhalter gebunden. Was gibt das aus?
import sqlite3
con = sqlite3.connect(":memory:")
print(con.execute("SELECT ?", ("x' OR '1'='1",)).fetchone())// A5.4 · ca. 35 Min. · Vertiefung
Nach dieser Lektion machen Sie aus Text Daten mit ast.literal_eval oder json statt eval, erklären, warum Unpickling fremder Bytes Code ausführen kann, und starten andere Programme mit subprocess.run und einer Argumentliste statt über eine Shell.
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
import sqlite3
con = sqlite3.connect(":memory:")
print(con.execute("SELECT ?", ("x' OR '1'='1",)).fetchone())Vorhersagen · Aufgabe 2 von 7
text = "print('I ran') or 42" # typed by a user
print(eval(text)) # the mistake: eval runs any expressionÜben · Aufgabe 3 von 7
import ast
text = "{'a': [1, 2], 'b': None}"
data = ast.____(text)
print(data["a"][1], data["b"])Üben · Aufgabe 4 von 7
import pickle
class Loud:
def __reduce__(self):
return (print, ("unpickling ran print",))
data = pickle.dumps(Loud())
result = pickle.loads(data)
print(result)Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
import ast
for text in ["-5", "(1, 'a')", "2 + 3", "{1, 2}"]:
try:
print(ast.literal_eval(text), end=" ")
except ValueError:
print("rejected", end=" ")Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Das Programm liest ein Einstellungs-Literal mit ast.literal_eval und zeigt, dass ein im Text versteckter Aufruf abgelehnt wird. Dann schickt es Daten einmal durch json hin und zurück. Zuletzt entpickelt es ein Alarm-Objekt, dessen __reduce__ print nennt: Die Meldung erscheint während pickle.loads, bevor Ihr Code überhaupt ein Objekt sieht. Ein präpariertes Pickle kann genauso eine schädliche Funktion nennen; hier gibt es nur etwas aus.
main.py
import ast
import json
import pickle
class Alarm:
"""Stands in for a crafted object: unpickling it calls print."""
def __reduce__(self):
return (print, ("!! code ran while unpickling",))
# 1. Text holding a Python literal: ast.literal_eval, never eval.
settings = ast.literal_eval("{'size': (800, 600), 'tags': ['a', 'b'], 'debug': False}")
print(settings["size"][0] * settings["size"][1], settings["tags"])
try:
ast.literal_eval("__import__('os').getcwd()")
except ValueError as err:
print("rejected:", str(err).split(":")[0])
# 2. Data from outside the program: json can only build data.
text = json.dumps({"user": "ada", "scores": [3, 5]})
print(text)
print(json.loads(text)["scores"])
# 3. pickle can call functions while loading: only for data you trust.
blob = pickle.dumps(Alarm())
pickle.loads(blob)
Ausführen mit
python main.pyAusgabe
480000 ['a', 'b']
rejected: malformed node or string on line 1
{"user": "ada", "scores": [3, 5]}
[3, 5]
!! code ran while unpicklingTab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übung 1 von 3
parse_settings(text) nutzt eval, Text wie "{'n': len('abc')}" führt also Code aus. Schreiben Sie es mit ast.literal_eval neu. Lösen Sie ValueError aus, wenn der Text kein Literal ist, und auch, wenn das Ergebnis kein dict ist, dessen Schlüssel alle Strings sind.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
import ast, dann value = ast.literal_eval(text). Bei Aufrufen und Namen löst es schon ValueError aus.
Prüfen Sie nach dem Parsen isinstance(value, dict) und all(isinstance(key, str) for key in value).
Schlägt eine der Prüfungen fehl, lösen Sie selbst ValueError aus.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import ast
def parse_settings(text: str) -> dict[str, object]:
"""Turn text such as "{'width': 80, 'tags': ['a']}" into a dict."""
value = ast.literal_eval(text) # ValueError for anything that is not a literal
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise ValueError("settings must be a dict with str keys")
return value
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
def parse_settings(text: str) -> dict[str, object]:
"""Turn text such as "{'width': 80, 'tags': ['a']}" into a dict."""
# eval runs any expression in text. Use ast.literal_eval, and raise
# ValueError unless the result is a dict with only str keys.
return eval(text)
test_main.py
from main import parse_settings
def rejects(text):
try:
parse_settings(text)
except ValueError:
return True
return False
def test_literal_dict():
"""Ein dict aus Literalen wird geparst"""
got = parse_settings("{'width': 80, 'tags': ['a', 'b'], 'theme': None}")
assert got == {"width": 80, "tags": ["a", "b"], "theme": None}, f"parse_settings lieferte {got!r}"
def test_rejects_call():
"""Ein Funktionsaufruf im Text wird abgelehnt, nicht ausgeführt"""
assert rejects("{'n': len('abc')}"), "{'n': len('abc')} wurde angenommen: eval hat len() ausgeführt; nutzen Sie ast.literal_eval"
def test_rejects_name():
"""Ein Variablenname im Text wird abgelehnt"""
assert rejects("{'n': width}"), "ein Name im Text wurde angenommen oder löste den falschen Fehler aus"
def test_rejects_non_dict():
"""Eine Liste ist keine Einstellung"""
assert rejects("[1, 2]"), "[1, 2] wurde angenommen; lösen Sie ValueError aus, wenn das Ergebnis kein dict ist"
def test_rejects_non_str_key():
"""Schlüssel müssen Strings sein"""
assert rejects("{1: 'a'}"), "{1: 'a'} wurde angenommen; jeder Schlüssel muss ein str sein"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 3
dump_scores und load_scores tauschen Punkte mit anderen Programmen als hexkodierte Pickles aus, load_scores würde also ausführen, was ein präpariertes Pickle nennt. Stellen Sie beide auf json um: dump_scores liefert JSON-Text, und load_scores löst ValueError aus, wenn der Text kein JSON-Objekt ist, dessen Werte alle Ganzzahlen sind (keine Wahrheitswerte).
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
json.dumps(scores) und json.loads(text). json.JSONDecodeError ist eine Unterklasse von ValueError, schlechter Text löst ihn also schon aus.
Ein Hex-String ist kein gültiges JSON, ein Pickle wird also abgelehnt, bevor etwas darin laufen könnte.
Prüfen Sie isinstance(data, dict), dann jeden Wert mit isinstance(v, int) and not isinstance(v, bool).
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import json
def dump_scores(scores: dict[str, int]) -> str:
"""Save scores as text that another program can read back."""
return json.dumps(scores)
def load_scores(text: str) -> dict[str, int]:
"""Read scores saved by dump_scores; raise ValueError for anything else."""
data = json.loads(text) # json.JSONDecodeError is a ValueError
if not isinstance(data, dict):
raise ValueError("scores must be a JSON object")
for name, points in data.items():
if not isinstance(points, int) or isinstance(points, bool):
raise ValueError(f"score for {name!r} is not an integer")
return data
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import pickle
def dump_scores(scores: dict[str, int]) -> str:
"""Save scores as text that another program can read back."""
return pickle.dumps(scores).hex()
def load_scores(text: str) -> dict[str, int]:
"""Read scores saved by dump_scores; raise ValueError for anything else."""
return pickle.loads(bytes.fromhex(text))
test_main.py
import json
import pickle
from main import dump_scores, load_scores
class Alarm:
def __reduce__(self):
return (print, ("alarm",))
def rejects(text):
try:
load_scores(text)
except ValueError:
return True
return False
def test_round_trip():
"""Punkte überstehen Speichern und Laden"""
scores = {"ada": 3, "bob": 5}
got = load_scores(dump_scores(scores))
assert got == scores, f"load_scores(dump_scores(...)) lieferte {got!r}"
def test_readable_json():
"""dump_scores schreibt JSON, das jedes Programm lesen kann"""
text = dump_scores({"ada": 3})
try:
got = json.loads(text)
except ValueError:
got = None
assert got == {"ada": 3}, f"dump_scores schrieb {text[:40]!r}, und das ist kein JSON"
def test_refuses_pickle():
"""Ein Pickle wird mit ValueError abgelehnt, und nichts darin läuft"""
blob = pickle.dumps(Alarm()).hex()
assert rejects(blob), "load_scores hat ein Pickle angenommen: Es könnte beim Laden Code ausführen"
def test_checks_types():
"""Eine Punktzahl, die keine Ganzzahl ist, wird abgelehnt"""
assert rejects('{"ada": "three"}'), 'load_scores hat {"ada": "three"} angenommen'
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 3 von 3
child_args(words) startet einen zweiten Python-Prozess und liefert die Argumente, die er bekommen hat. Der Starter baut einen Shell-Befehl, "two words" zerfällt also in zwei, $HOME wird ersetzt, und ein Apostroph zerbricht den Befehl. Übergeben Sie subprocess.run stattdessen eine Liste, mit check=True. Führen Sie die Tests auf Ihrem eigenen Rechner aus: Der Browser kann keine Prozesse starten.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Die Liste ist [sys.executable, "-c", CHILD, *words]: Jedes Wort wird genau ein Argument.
Lassen Sie shell=True weg; subprocess nutzt keine Shell, außer Sie verlangen sie.
Behalten Sie capture_output=True und text=True, damit result.stdout ein str für json.loads ist.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import json
import subprocess
import sys
CHILD = "import json, sys; print(json.dumps(sys.argv[1:]))"
def child_args(words: list[str]) -> list[str]:
"""Run CHILD in a new Python process with words as its arguments; return what it received."""
result = subprocess.run(
[sys.executable, "-c", CHILD, *words],
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout)
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import json
import subprocess
import sys
CHILD = "import json, sys; print(json.dumps(sys.argv[1:]))"
def child_args(words: list[str]) -> list[str]:
"""Run CHILD in a new Python process with words as its arguments; return what it received."""
# A shell splits, expands and interprets the words. Pass a list instead.
command = f'"{sys.executable}" -c "{CHILD}" ' + " ".join(words)
result = subprocess.run(command, shell=True, capture_output=True, text=True)
return json.loads(result.stdout)
test_main.py
from main import child_args
def test_simple():
"""Zwei einfache Wörter kommen als zwei Argumente an"""
got = child_args(["a", "b"])
assert got == ["a", "b"], f"der Kindprozess bekam {got!r}"
def test_spaces():
"""Ein Wort mit Leerzeichen bleibt ein Argument"""
got = child_args(["two words"])
assert got == ["two words"], f"der Kindprozess bekam {got!r}: keine Shell, übergeben Sie eine Liste"
def test_special_characters():
"""$HOME und ein Apostroph kommen unverändert an"""
got = child_args(["$HOME", "it's"])
assert got == ["$HOME", "it's"], f"der Kindprozess bekam {got!r}: eine Shell hat sie ersetzt oder zerbrochen"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenimport ast
size = 3
config = ast.literal_eval("[1, 2, size]")
Was Python ausgibt
ValueError: malformed node or string on line 1: Name(id='size', ctx=Load())Warum, und die Lösung
literal_eval kennt keine Variablen: size kann es nicht nachschlagen. Genau das ist gewollt, greifen Sie also nicht zu eval. Parsen Sie den literalen Teil und verbinden Sie ihn im Code, etwa ast.literal_eval("[1, 2]") + [size].
import json
tags = {"python", "security"}
print(json.dumps({"tags": tags}))
Was Python ausgibt
TypeError: Object of type set is not JSON serializableWarum, und die Lösung
JSON kennt Objekte, Arrays, Strings, Zahlen, true, false und null, aber keine Mengen, Tupel oder eigenen Klassen. Wandeln Sie vor dem Speichern um, etwa sorted(tags), und nach dem Laden zurück. Genau diese Grenze macht json sicher: Laden kann nur einfache Daten bauen.
import subprocess
import sys
count = 3
subprocess.run([sys.executable, "-c", "import sys; print(sys.argv)", count])
Was Python ausgibt
TypeError: expected str, bytes or os.PathLike object, not intWarum, und die Lösung
Jedes Element der Argumentliste wird ein Kommandozeilenargument, und die sind immer Text. Wandeln Sie vorher um: str(count). Stattdessen einen Befehlsstring per f-String zu bauen, brächte die Shell-Probleme zurück.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.