Zum Inhalt springen
aviral gupta

// A5.4 · ca. 35 Min. · Vertiefung

eval, pickle und shell=True

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.

Lektion 4 von 5 in A5 Modernes Python und sicherer Code

Danach können Sie

  • eval durch ast.literal_eval oder json ersetzen, wenn aus Text Daten werden
  • Erklären, warum Unpickling fremder Daten Code ausführen kann, und für Daten von außen json nutzen
  • Andere Programme mit subprocess.run und einer Argumentliste starten statt mit shell=True
  1. 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())
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Ein Programm „berechnet“ die Eingabe mit eval. Was gibt das aus?

    text = "print('I ran') or 42"  # typed by a user
    print(eval(text))  # the mistake: eval runs any expression
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die ast-Funktion ein, die den Text in Daten verwandelt, ohne Code auszuführen.

    import ast
    
    text = "{'a': [1, 2], 'b': None}"
    data = ast.____(text)
    print(data["a"][1], data["b"])
    data = ast.(text)
  4. Üben · Aufgabe 4 von 7

    Eine Klasse bestimmt, wie sie entpickelt wird. Was gibt das aus?

    import pickle
    
    
    class Loud:
        def __reduce__(self):
            return (print, ("unpickling ran print",))
    
    
    data = pickle.dumps(Loud())
    result = pickle.loads(data)
    print(result)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jeder Aufgabe das Werkzeug zu, das sie sicher erledigt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Welche Eingaben akzeptiert literal_eval? Was gibt das aus?

    import ast
    
    for text in ["-5", "(1, 'a')", "2 + 3", "{1, 2}"]:
        try:
            print(ast.literal_eval(text), end=" ")
        except ValueError:
            print("rejected", end=" ")
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Ein Formular schickt einen Punkt als Text, etwa "(3, 4.5)". Schreiben Sie parse_point(text): Es liefert das Tupel und löst ValueError aus, wenn der Text kein Tupel aus genau zwei Zahlen ist (keine bools). Nutzen Sie ast.literal_eval, nie eval, und probieren Sie "(1, 2, 3)", "('a', 1)" und "max(1, 2)".

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

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

Drei Arten, Daten zu laden, nebeneinander

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.py

Ausgabe

480000 ['a', 'b']
rejected: malformed node or string on line 1
{"user": "ada", "scores": [3, 5]}
[3, 5]
!! code ran while unpickling
  • literal_eval baut Tupel, Listen, dicts und Wahrheitswerte, sonst nichts.
  • Der Aufruf __import__('os') wurde mit ValueError abgelehnt, nicht ausgeführt.
  • json.dumps schreibt den Text; json.loads kann daraus nur Daten bauen.
  • Die letzte Zeile gibt pickle.loads selbst aus.
Ändern und ausführen

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.

Übungen

Übung 1 von 3

Einstellungen ohne eval

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.

Hinweise
  1. Hinweis 1

    import ast, dann value = ast.literal_eval(text). Bei Aufrufen und Namen löst es schon ValueError aus.

  2. Hinweis 2

    Prüfen Sie nach dem Parsen isinstance(value, dict) und all(isinstance(key, str) for key in value).

  3. Hinweis 3

    Schlägt eine der Prüfungen fehl, lösen Sie selbst ValueError aus.

Eine Lösung zeigen

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
Auf dem eigenen Computer ausführen

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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Punkte als JSON, nicht als Pickle

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.

Hinweise
  1. Hinweis 1

    json.dumps(scores) und json.loads(text). json.JSONDecodeError ist eine Unterklasse von ValueError, schlechter Text löst ihn also schon aus.

  2. Hinweis 2

    Ein Hex-String ist kein gültiges JSON, ein Pickle wird also abgelehnt, bevor etwas darin laufen könnte.

  3. Hinweis 3

    Prüfen Sie isinstance(data, dict), dann jeden Wert mit isinstance(v, int) and not isinstance(v, bool).

Eine Lösung zeigen

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
Auf dem eigenen Computer ausführen

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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

Argumente ohne Shell

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.

Hinweise
  1. Hinweis 1

    Die Liste ist [sys.executable, "-c", CHILD, *words]: Jedes Wort wird genau ein Argument.

  2. Hinweis 2

    Lassen Sie shell=True weg; subprocess nutzt keine Shell, außer Sie verlangen sie.

  3. Hinweis 3

    Behalten Sie capture_output=True und text=True, damit result.stdout ein str für json.loads ist.

Eine Lösung zeigen

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)
Auf dem eigenen Computer ausführen

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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Ein Name im Literal

import 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].

Von pickle auf json umstellen, mit einer Menge

import json

tags = {"python", "security"}
print(json.dumps({"tags": tags}))

Was Python ausgibt

TypeError: Object of type set is not JSON serializable

Warum, 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.

Eine Zahl in der Argumentliste

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 int

Warum, 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

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

eval führt Code aus; literal_eval liest nur Literale

eval(text) wertet jeden Ausdruck aus, Text von Nutzern, aus Dateien oder dem Netz kann also jede Funktion aufrufen, die Ihr Programm erreicht. Die Doku warnt vor Sicherheitslücken und stellt klar, dass ein eingeschränktes __builtins__ kein Schutzmechanismus ist. Um Text in Daten zu verwandeln, nehmen Sie ast.literal_eval: Es akzeptiert nur Literale (Strings, bytes, Zahlen, Tupel, Listen, dicts, Mengen, Wahrheitswerte, None und Ellipsis) und löst bei Namen, Aufrufen und Operatoren ValueError aus. Es führt nie Code aus, sehr große Eingaben können aber den Speicher erschöpfen; für Daten von außen ist json das bessere Format.

Unpickling kann Funktionen aufrufen

pickle kann fast jedes Python-Objekt speichern, denn beim Unpickling wird es durch Aufrufe von Funktionen neu gebaut, die die Daten selbst benennen. Eine Klasse wählt sie mit __reduce__; hier liefert es (print, ("...",)), also ruft pickle.loads print auf. Ein präpariertes Pickle könnte ebenso jede andere Funktion nennen. Die Doku ist deutlich: Nie Daten unpickeln, die aus einer fremden Quelle stammen oder manipuliert sein könnten. Nutzen Sie pickle nur für Daten, die Ihr eigenes Programm geschrieben und sicher verwahrt hat; für Daten über Grenzen hinweg nehmen Sie json, das nur dicts, Listen, Strings, Zahlen, Wahrheitswerte und None bauen kann.

subprocess.run mit einer Liste, ohne Shell

subprocess.run(["prog", "arg one", name]) startet prog direkt, und jedes Listenelement kommt als genau ein Argument an: Leerzeichen, Anführungszeichen, $ und ; sind nur Zeichen. subprocess ruft nie eine Shell auf, außer Sie übergeben shell=True. Mit shell=True und einem aus Strings gebauten Befehl trennt die Shell Wörter, ersetzt $HOME und liest ; als „noch einen Befehl ausführen“: Shell-Injection. check=True löst bei einem Status ungleich 0 CalledProcessError aus, capture_output=True, text=True liefert stdout als str. Prozesse starten braucht Ihr eigenes Python: Im Browser läuft subprocess nicht.

Quellen

Zuletzt geprüft am 29. September 2026