flawopen.com/xss/Python
XSS (Cross-Site-Scripting) in Python verhindern (Flask, Jinja2, Django)
Wer Nutzereingaben in Markup, mark_safe oder einen an render_template_string übergebenen f-String packt, lässt den Text eines Besuchers als JavaScript im Browser anderer Nutzer laufen – mit deren Sitzung (CWE-79). Wo Flask, Jinja2 und Django für Sie maskieren, die vier Stellen, an denen sie es nicht tun, und wie man Rich Text sicher zulässt.
Stell dir eine Druckerei mit einem geschützten Formular vor: Text im Leerfeld wird als normaler Text gedruckt. Klebt ein Mitarbeiter jedoch die Notiz eines Kunden vor dem Druck in das Anweisungsblatt des Druckers, kann die Notiz zum Druckbefehl werden. Eine Python-f-string in render_template_string macht genau das: Sie setzt die Eingabe in den Jinja2-Quelltext ein, bevor autoescape sie als Variable behandeln kann. Markup und mark_safe sind wie ein Stempel „HTML bereits geprüft“; sie prüfen nichts. Lass die Eingabe in Template-Variablen und autoescape wirken oder verwende format_html für ein kleines Django-Fragment.
Kernkonzepte & Begriffe
Jinja2 autoescape- Flask aktiviert Jinja2-Autoescaping für
.html-,.htm-,.xml- und.xhtml-Templates sowie fürrender_template_string, aus{{ q }}wird also<img ...>. Ein eigenständigesjinja2.Environment()hat Autoescaping ausgeschaltet, sofern Sie nichtautoescape=select_autoescape()übergeben. Markup / |safe / mark_safe- Alle drei markieren einen String als bereits sicheres HTML, und die Template-Engine maskiert ihn nicht mehr. Das sind Versprechen des Entwicklers, keine Sanitizer:
Markup(user_input)odermark_safe(user_input)veröffentlicht, was der Nutzer eingegeben hat. render_template_string(f"...")- Ein f-String setzt den Wert in den Template-Quelltext, bevor Jinja2 ihn parst, das Autoescaping sieht ihn also nie als Variable. Dieselbe Zeile ist zudem Server-Side Template Injection: Ein Wert wie
{{ 7*7 }}wird von Jinja2 ausgewertet. django.utils.html.format_html- Baut HTML wie
str.format, maskiert aber zuerst jedes Argument mitconditional_escapeund markiert dann das Ergebnis als sicher. So baut man in Django ein kleines HTML-Fragment in Python-Code – statt mitmark_safe(f"..."). nh3.clean()- Rich Text braucht einen Sanitizer, kein Escaping.
nh3.clean()(Rusts ammonia) führt eine Allowlist von Tags und Attributen und entfernt<script>, Event-Handler undjavascript:-URLs. Bleach ist veraltet; nh3 ist der gepflegte Nachfolger.
Framework-Zuordnungen
- CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Schritt-für-Schritt Angriffsablauf
Die Eingabe kommt mit einem Request
Ein Suchfeld, ein Profilformular oder ein Link-Parameter schickt q, bio oder next an den Server. Dem Opfer kann ein präparierter Link geschickt werden, oder der Wert wird gespeichert und jedem Profilbesucher angezeigt.
Der Wert umgeht das Escaping
Ein f-String setzt ihn in den Template-Quelltext, Markup() oder mark_safe() erklären ihn für vertrauenswürdig, oder er landet in einem href, wo das Maskieren von < und > nicht reicht. Der harmlose Testwert <img src=x onerror=alert(1)> kommt unverändert durch.
Der Browser parst ihn als Markup
Die Antwort enthält <img src=x onerror=alert(1)> als echtes Element. Wenn die Bildressource nicht geladen werden kann, wird onerror ausgelöst, und eine javascript:-URL im Link läuft beim Klick.
Das Skript läuft im Namen der Website
In den gezeigten Flask/Jinja- und Django-Pfaden wird ausgegebenes Markup unter dem Origin der Anwendung ausgeführt. Nicht vertrauenswürdige Werte gehören in automatisch maskierte Template-Datenpositionen; HTML ist nur nach kontextgerechter Bereinigung zulässig.
Quellcode: Verwundbar vs. Sicher
# views.py: jeder Handler unten lässt Request-Text als HTML in die Seite
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup
app = Flask(__name__)
# der f-String baut Template-Quelltext, Jinja2 maskiert q also nie
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string(f"<h1>Results for {q}</h1>")
# Markup() sagt Jinja2, dass die Bio vertrauenswürdiges HTML ist
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))
# mark_safe auf einem f-String veröffentlicht name als HTML
def greeting(name):
return mark_safe(f"<p>Hello, {name}</p>")
# Autoescaping stoppt keine javascript:-URL in href
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
# views.py: Werte bleiben Template-Variablen; Rich Text wird bereinigt
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request
app = Flask(__name__)
# q ist eine Template-Variable, Autoescaping kodiert sie
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string("<h1>Results for {{ q }}</h1>", q=q)
# nh3 wendet eine Tag-Allowlist an, bevor dem HTML vertraut wird
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio|safe }}</div>", bio=nh3.clean(bio))
# format_html maskiert jedes Argument, bevor das Ergebnis als sicher gilt
def greeting(name):
return format_html("<p>Hello, {}</p>", name)
# nur Pfade der eigenen Website sind als Linkziel erlaubt
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
if not url.startswith("/") or url.startswith("//"):
url = "/"
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
Checkliste für Engineering & Systemsicherheit
- ✓Das CI bei Bandit B703/B308 (
mark_safe) und B704 (Markupauf Nicht-Literalen) scheitern lassen, ebenso bei jedemrender_template_string, dessen erstes Argument ein f-String,%oder ein.format()-Ergebnis ist. - ✓Werte als Variablen an Templates übergeben und das Autoescaping arbeiten lassen; bei eigenständigem Jinja2
Environment(autoescape=select_autoescape())anlegen. - ✓Für kleine Fragmente
format_html()stattmark_safe()verwenden und für jedes nutzergelieferte HTMLnh3.clean(). - ✓Jede nutzergesteuerte URL prüfen, bevor sie
hrefodersrcerreicht: nur Pfade der eigenen Website mit genau einem führenden/oder eine explizite http/https-Allowlist akzeptieren. - ✓Eine Content-Security-Policy mit Nonce-basiertem
script-srcohne'unsafe-inline'senden und Session-CookiesHttpOnlysowieSameSite=Laxsetzen.
Quellen
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Flask — Security Considerations: Cross-Site Scripting
- Django — Security: Cross site scripting (XSS) protection
- Django — format_html
- nh3 — Python bindings to the ammonia HTML sanitizer
- OWASP Cross Site Scripting Prevention Cheat Sheet
- nh3 — Python bindings to the ammonia HTML sanitizer