Mengeraskan aplikasi Flask dari serangan web yang umum: XSS dengan autoescape dan sanitasi, CSRF dengan token, SQL injection lewat ORM dan parameter binding, clickjacking, hingga secure headers dengan Flask-Talisman dan praktik hardening menyeluruh.

Aplikasi web tidak hidup sendirian — ia menghadapi internet yang berisi penyerang. Episode 17 mengubah pola pikir dari "membuat fitur" menjadi "mengamankan fitur": kita membahas lima serangan paling umum terhadap web app — XSS, CSRF, SQL injection, clickjacking, dan header tidak aman — beserta pertahanannya di Flask.
Sebagian pertahanan ini sudah kita sentuh di episode sebelumnya (autoescape di episode 6, CSRF di episode 12). Episode 17 merangkai semuanya menjadi strategi keamanan yang menyeluruh.
XSS terjadi saat input pengguna dirender sebagai kode HTML/JavaScript. Contoh klasik: kolom komentar yang menerima <script> lalu mengeksekusi-nya di browser pengunjung lain.
Pertahanan berlapis di Flask:
Lapisan 1 — Autoescape (sudah bawaan). Jinja2 meng-escape output {{ }} secara default (episode 6). Jangan matikan tanpa alasan:
<p>{{ comment.content }}</p>Lapisan 2 — Jangan pakai |safe untuk input pengguna. Untuk konten yang memang HTML (artikel markdown), sanitisasi di server dengan library seperti bleach:
import bleach
ALLOWED_TAGS = ["p", "b", "i", "a", "ul", "ol", "li", "code", "pre"]
ALLOWED_ATTRS = {"a": ["href", "title"]}
clean_html = bleach.clean(raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)bleach.clean() menghapus tag dan atribut di luar daftar — menghilangkan <script> dan onerror sebelum HTML sampai ke template.
CSRF (episode 12) membuat browser korban mengirim request yang tidak dikehendaki. Pertahanan Flask-WTF (CSRFProtect) sudah aktif otomatis di aplikasi kita:
from flask_wtf import CSRFProtect
csrf = CSRFProtect()
csrf.init_app(app) # di create_app()Jika ada endpoint API yang memakai cookie auth dan dikonsumsi browser dari domain lain, pastikan token CSRF tetap diverifikasi — jangan hanya mengandalkan CORS (episode 14).
SQL injection terjadi saat input pengguna disisipkan langsung ke string SQL. Karena kita memakai SQLAlchemy ORM, sebagian besar sudah aman — asalkan tidak menulis SQL mentah tanpa parameter:
# AMAN - parameter binding lewat ORM
users = db.session.scalars(
db.select(User).where(User.username == form.username.data)
).all()
# AMAN - text() dengan parameter
rows = db.session.execute(
db.text("SELECT * FROM users WHERE email = :email"),
{"email": email},
)
# BERBAHAYA - f-string ke dalam SQL
rows = db.session.execute(db.text(f"SELECT * FROM users WHERE email = '{email}'"))Aturan emas: jangan pernah membangun query SQL dengan f-string atau concatenation. Selalu parameter binding (:nama atau ?) atau ORM. Dengan ORM, input pengguna diperlakukan sebagai data, bukan struktur query.
Warning
Bahaya SQL injection tidak terbatas pada ORM — menyangkut seluruh permukaan database. Waspada juga di query text() mentah, filter dinamis dari request (sorting, search), dan tooling admin. Satu aturan: jika nilainya berasal dari luar kode, ia hanya boleh masuk sebagai parameter, bukan string SQL.
Clickjacking memaksa pengguna mengklik elemen tak terlihat di dalam iframe berbahaya. Pertahanannya satu header:
@app.after_request
def add_security_headers(response):
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return responseX-Frame-Options: DENY dan Content-Security-Policy: frame-ancestors 'none' mencegah halaman kalian ditampilkan di iframe situs lain. Kita kembangkan header lengkap di bawah.
Semua header keamanan bisa dirapikan dengan Flask-Talisman, extension khusus:
pip install flask-talismanfrom flask_talisman import Talisman
talisman = Talisman(
app,
force_https=True, # redirect semua HTTP ke HTTPS
strict_transport_security=True, # HSTS
session_cookie_secure=True,
content_security_policy={
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self'",
},
frame_options="DENY",
)Sekaligus mengatur: paksa HTTPS, HSTS, cookie aman, dan Content-Security-Policy (CSP). CSP adalah salah satu pertahanan terkuat melawan XSS: ia membatasi sumber yang boleh dimuat browser. Efek sampingnya langsung terasa — script/style dari CDN atau inline akan diblokir, jadi sesuaikan dengan kebutuhan halaman kalian (kita sentuh lagi di episode 20).
Rangkuman lapisan pertahanan yang sudah dan akan kita bangun:
| Serangan | Pertahanan | Episode |
|---|---|---|
| XSS | Autoescape + sanitasi + CSP | 6, 17 |
| CSRF | CSRFProtect + token di semua form | 12 |
| SQL injection | ORM + parameter binding | 10, 17 |
| Clickjacking | X-Frame-Options + frame-ancestors | 17 |
| Password bocor | generate_password_hash (scrypt) | 13 |
| Brute force login | Rate limit | 15 |
| Session palsu | SECRET_KEY kuat + cookie flags | 8, 20 |
| DoS via form | MAX_FORM_MEMORY_SIZE/MAX_CONTENT_LENGTH | 3, 17 |
Dua konfigurasi dari Flask 3.1 yang sebaiknya selalu diaktifkan:
class BaseConfig:
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16 MB maksimal
MAX_FORM_MEMORY_SIZE = 1024 * 1024 # 1 MB form di memori
MAX_FORM_PARTS = 100 # maks 100 bagian formMAX_CONTENT_LENGTH membatasi besar body (menolak upload gede otomatis dengan 413); MAX_FORM_MEMORY_SIZE dan MAX_FORM_PARTS (baru di 3.1) membatasi form yang bisa membanjiri memori.
|safe pada input pengguna: membuka XSS — sanitasikan dulu dengan bleach.default-src 'self'.MAX_CONTENT_LENGTH tak diset: request raksasa bisa membebani server.Pada episode 17 ini, kalian telah mengeraskan aplikasi dengan keamanan berlapis.
Inti yang harus dibawa pulang:
bleach untuk konten HTML + CSP — jangan |safe input pengguna.CSRFProtect otomatis; token wajib di semua form POST.X-Frame-Options dan frame-ancestors.MAX_CONTENT_LENGTH, MAX_FORM_*).Di episode 18 selanjutnya, kita melengkapi autentikasi untuk API modern: JWT & OAuth — Flask-JWT-Extended dengan access/refresh token dan RBAC, serta social login dengan Authlib untuk SPA dan mobile. Sampai jumpa di episode 18!