FlawOpen

flawopen.com/xss/Python

● CWE-79 · Tinggi
Riset Keamanan · FlawOpen

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.

💡 Penjelasan Sederhana (ELI5)

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 .xhtml serta untuk render_template_string, sehingga {{ q }} menjadi <img ...>. jinja2.Environment() yang berdiri sendiri mematikan autoescape kecuali Anda memberikan autoescape=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) atau mark_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 dengan conditional_escape, lalu menandai hasilnya aman. Inilah cara yang disarankan Django untuk menyusun potongan HTML kecil di kode Python, alih-alih mark_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 URL javascript:. Bleach sudah deprecated; nh3 adalah penerusnya yang masih dirawat.

Pemetaan kerangka kerja

Alur Serangan Langkah demi Langkah

Langkah 1

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.

Langkah 2

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.

Langkah 3

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.

Langkah 4

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

IMPLEMENTASI RENTAN
# 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)
PERBAIKAN AMAN & KUAT
# 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

Sumber