Cross-Site Request Forgery (CSRF, CWE-352): cara kerja dan cara mencegahnya
Halaman di situs lain bisa membuat browser pengguna yang sedang login mengirim request pengubah data ke aplikasi Anda, dan browser melampirkan cookie sesi secara otomatis, sehingga route yang hanya memeriksa cookie menjalankan tindakan itu. Bagaimana token sinkronisasi, cookie SameSite, dan pemeriksaan Sec-Fetch-Site menghentikannya, dengan contoh Flask dan Flask-WTF.
Penjelasan Sederhana (ELI5)
Kantor Anda punya ruang surat yang menjalankan setiap slip permintaan bertanda tangan di baki keluar Anda, dan mempercayainya karena slip itu ada di baki Anda, yang hanya bisa Anda jangkau. Seorang tamu meminta Anda memasukkan amplop tertutup ke baki saat Anda keluar. Di dalamnya ada slip bertuliskan 'ubah alamat pengiriman semua paket saya'. Ruang surat melihatnya di baki Anda, mengira Anda yang menulisnya, dan menjalankannya. CSRF bekerja sama persis: browser menempelkan cookie sesi Anda pada setiap request ke bank Anda, termasuk yang dimulai oleh situs web lain. Perbaikannya adalah cap rahasia yang hanya bisa dibubuhkan halaman dari kantor Anda sendiri, sehingga ruang surat menolak slip tanpa cap itu.
Konsep Kunci & Istilah
- Kredensial ambien
- Browser melampirkan cookie dan autentikasi HTTP ke request menuju sebuah situs, apa pun halaman yang memulainya. Request dari
evil.examplekeapp.exampletetap membawa cookie sesiapp.examplekecuali atributSameSitecookie itu mencegahnya. - Atribut SameSite pada cookie
Strictmenahan cookie pada setiap request lintas situs,Laxhanya mengirimnya pada navigasi GET tingkat atas, danNoneselalu mengirimnya (dan mewajibkanSecure). Chromium memperlakukan cookie tanpa atribut sebagaiLax; Firefox dan Safari tidak, dan 'situs yang sama' juga mencakup subdomain saudara.- Token sinkronisasi
- Nilai acak yang terikat pada sesi, yang dipasang server di setiap formulir atau halaman dan diwajibkan pada setiap request tidak aman. Situs lain tidak bisa membacanya, sehingga tidak bisa memalsukan request yang sah.
CSRFProtectdari Flask-WTF menerbitkannya lewatcsrf_token()dan memeriksa fieldcsrf_tokenatau headerX-CSRFToken. - Fetch Metadata (Sec-Fetch-Site)
- Browser modern mengirim
Sec-Fetch-Site: cross-site,same-site,same-origin, ataunonepada setiap request, dan halaman tidak bisa memalsukannya. Menolak metode tidak aman yang bukansame-originmemblokir CSRF dengan satu pemeriksaan di server. - Mengubah state lewat GET
- Route yang mengubah data lewat GET bisa dipicu dengan tag
<img>atau tautan, danSameSite=Laxtetap mengirim cookie pada navigasi GET tingkat atas. Membacarequest.valuesmenerima query string selain body formulir.
Alur Serangan Langkah demi Langkah
Korban sedang login
Korban punya cookie sesi untuk app.example, yang dikirim dengan SameSite=None atau tanpa atribut di browser yang tidak memakai Lax secara default.
Korban membuka halaman milik penyerang
Halaman itu berisi formulir tersembunyi yang dikirim ke https://app.example/account/email dan terkirim sendiri saat dimuat. Formulir yang mengirim nilai uji tidak berbahaya email=test@example.com menggambarkan kasus ini.
Browser mengirim request beserta cookie
Request membawa sesi korban, dan server tidak melihat apa pun yang menunjukkan bahwa formulir itu berasal dari situs lain.
Server mengubah akun
Alamat email berganti menjadi milik penyerang, yang lalu mereset kata sandi dan mengambil alih akun. Pada 2008 Zeller dan Felten menunjukkan celah CSRF semacam ini di ING Direct, YouTube, dan The New York Times.
Kode Sumber: Rentan vs Aman
# app.py: cookie sesi adalah satu-satunya bukti bahwa pengguna menghendakinya
import os
from flask import Flask, abort, request, session
from .db import db
app = Flask(__name__)
app.config.update(
SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_SAMESITE="None",
)
# GET juga jalan, dan request.values ikut membaca query string
@app.route("/account/email", methods=["GET", "POST"])
def change_email():
user_id = session.get("user_id")
if user_id is None:
abort(401)
db.execute("UPDATE users SET email = ? WHERE id = ?", (request.values["email"], user_id))
return {"status": "ok"}
# app.py: token, cookie SameSite, dan Fetch Metadata bersama-sama
import os
from flask import Flask, abort, request, session
from flask_wtf.csrf import CSRFProtect
from .db import db
app = Flask(__name__)
app.config.update(
SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
# setiap POST butuh csrf_token; formulir merender {{ csrf_token() }}, fetch() mengirim X-CSRFToken
csrf = CSRFProtect(app)
# penulisan yang dimulai situs lain ditolak sebelum route berjalan
@app.before_request
def reject_cross_site_writes():
if request.method not in ("GET", "HEAD", "OPTIONS"):
if request.headers.get("Sec-Fetch-Site", "same-origin") not in ("same-origin", "none"):
abort(403)
# hanya POST, data dari body formulir
@app.post("/account/email")
def change_email():
user_id = session.get("user_id")
if user_id is None:
abort(401)
db.execute("UPDATE users SET email = ? WHERE id = ?", (request.form["email"], user_id))
return {"status": "ok"}
Daftar Periksa Penguatan Sistem Rekayasa
- Ubah state hanya lewat POST, PUT, PATCH, atau DELETE, dan baca data formulir dari body (
request.form), jangan darirequest.valuesatau query string. - Aktifkan middleware CSRF framework untuk seluruh aplikasi (
CSRFProtect,CsrfViewMiddlewareDjango,csrf()Spring Security) dan sertakan token di setiap formulir dan request AJAX. - Setel cookie sesi ke
Secure,HttpOnly, danSameSite=LaxatauStrict; pakaiSameSite=Nonehanya untuk cookie yang harus berfungsi di dalam iframe lintas situs. - Tolak request tidak aman yang header
Sec-Fetch-Site-nyacross-siteatausame-site. - Tambahkan tes CI yang mengirim request tanpa token ke setiap route pengubah state dan mengharapkan 400 atau 403.