flawopen.com/xss/Python
XSS (Cross-Site Scripting) di Python (Flask, Jinja2, Django)
Membungkus input pengguna dengan Markup, mark_safe, atau f-string yang diberikan ke render_template_string membuat teks pengunjung berjalan sebagai JavaScript di browser pengguna lain, dengan sesi mereka (CWE-79). Di mana Flask, Jinja2, dan Django meng-escape untuk Anda, empat tempat yang tidak, dan cara mengizinkan teks kaya dengan aman.
Bayangkan percetakan dengan formulir terlindungi: teks pada kolom kosong dicetak sebagai teks biasa. Namun, jika pegawai menempelkan catatan pelanggan ke lembar instruksi printer sebelum pencetakan dimulai, catatan itu bisa menjadi perintah cetak. f-string Python yang dikirim ke render_template_string melakukan hal tersebut: input disisipkan ke sumber Jinja2 sebelum autoescape dapat memperlakukannya sebagai variabel. Markup dan mark_safe seperti cap “HTML sudah diperiksa”, padahal keduanya tidak memeriksa apa pun. Biarkan input tetap sebagai variabel template agar autoescape bekerja, atau gunakan format_html untuk fragmen Django kecil.
Konsep Kunci & Istilah
Jinja2 autoescape- Flask mengaktifkan autoescape Jinja2 untuk template
.html,.htm,.xml, dan.xhtmlserta untukrender_template_string, sehingga{{ q }}menjadi<img ...>.jinja2.Environment()yang berdiri sendiri mematikan autoescape kecuali Anda memberikanautoescape=select_autoescape(). Markup / |safe / mark_safe- Ketiganya menandai string sebagai HTML yang sudah aman, sehingga mesin template berhenti meng-escape-nya. Ini janji dari developer, bukan sanitizer:
Markup(user_input)ataumark_safe(user_input)menerbitkan apa pun yang diketik pengguna. render_template_string(f"...")- f-string memasukkan nilai ke source template sebelum Jinja2 menguraikannya, sehingga autoescape tidak pernah melihatnya sebagai variabel. Baris yang sama juga merupakan server-side template injection: nilai seperti
{{ 7*7 }}dievaluasi oleh Jinja2. django.utils.html.format_html- Menyusun HTML seperti
str.format, tetapi lebih dulu meng-escape setiap argumen denganconditional_escape, lalu menandai hasilnya aman. Inilah cara yang disarankan Django untuk menyusun potongan HTML kecil di kode Python, alih-alihmark_safe(f"..."). nh3.clean()- Teks kaya membutuhkan sanitizer, bukan escaping.
nh3.clean()(ammonia dari Rust) memegang allowlist tag dan atribut serta membuang<script>, event handler, dan URLjavascript:. Bleach sudah deprecated; nh3 adalah penerusnya yang masih dirawat.
Pemetaan kerangka kerja
- Referensi CWE untuk kelemahan yang dijelaskan dalam panduan ini: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Alur Serangan Langkah demi Langkah
Input datang dalam request
Kotak pencarian, formulir profil, atau parameter tautan mengirim q, bio, atau next ke server. Korban bisa dikirimi tautan yang sudah dirancang, atau nilainya disimpan dan ditampilkan ke setiap orang yang melihat profil.
Nilai lolos dari escaping
f-string menaruhnya di source template, Markup() atau mark_safe() menandainya tepercaya, atau nilai itu masuk ke href, tempat meng-escape < dan > saja tidak cukup. Nilai uji yang tidak berbahaya <img src=x onerror=alert(1)> lolos utuh.
Browser menguraikannya sebagai markup
Respons berisi <img src=x onerror=alert(1)> sebagai elemen sungguhan. Jika sumber gambar gagal dimuat, onerror terpicu, dan URL javascript: di tautan berjalan saat diklik.
Skrip berjalan atas nama situs
Pada jalur Flask/Jinja dan Django yang ditampilkan, markup yang dikeluarkan berjalan pada origin aplikasi. Tempatkan nilai tidak tepercaya di posisi data template yang di-escape otomatis; izinkan HTML hanya setelah sanitasi sesuai konteks.
Kode Sumber: Rentan vs Aman
# views.py: setiap handler di bawah membiarkan teks request masuk ke halaman sebagai HTML
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup
app = Flask(__name__)
# f-string menyusun source template, jadi Jinja2 tidak pernah meng-escape q
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string(f"<h1>Results for {q}</h1>")
# Markup() memberi tahu Jinja2 bahwa bio adalah HTML tepercaya
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))
# mark_safe pada f-string menerbitkan name sebagai HTML
def greeting(name):
return mark_safe(f"<p>Hello, {name}</p>")
# autoescape tidak menghentikan URL javascript: di 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: nilai tetap berupa variabel template; teks kaya disanitasi
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request
app = Flask(__name__)
# q adalah variabel template, jadi autoescape meng-encode-nya
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string("<h1>Results for {{ q }}</h1>", q=q)
# nh3 menerapkan allowlist tag sebelum HTML dipercaya
@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 meng-escape setiap argumen sebelum menandai hasilnya aman
def greeting(name):
return format_html("<p>Hello, {}</p>", name)
# hanya path situs sendiri yang diizinkan sebagai tujuan tautan
@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)
Daftar Periksa Penguatan Sistem Rekayasa
- ✓Gagalkan CI pada Bandit B703/B308 (
mark_safe) dan B704 (Markuppada non-literal), serta pada setiaprender_template_stringyang argumen pertamanya berupa f-string,%, atau hasil.format(). - ✓Berikan nilai ke template sebagai variabel dan biarkan autoescape bekerja; untuk Jinja2 yang berdiri sendiri, buat
Environment(autoescape=select_autoescape()). - ✓Gunakan
format_html()alih-alihmark_safe()untuk potongan kecil, dannh3.clean()untuk setiap HTML dari pengguna. - ✓Validasi setiap URL yang dikendalikan pengguna sebelum sampai ke
hrefatausrc: hanya terima path situs sendiri yang diawali satu/, atau allowlist http/https yang eksplisit. - ✓Kirim Content-Security-Policy dengan
script-srcberbasis nonce tanpa'unsafe-inline', dan tandai cookie sesiHttpOnlysertaSameSite=Lax.
Sumber
- 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