● CWE-352 · OWASP A01:2021

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.example ke app.example tetap membawa cookie sesi app.example kecuali atribut SameSite cookie itu mencegahnya.
Atribut SameSite pada cookie
Strict menahan cookie pada setiap request lintas situs, Lax hanya mengirimnya pada navigasi GET tingkat atas, dan None selalu mengirimnya (dan mewajibkan Secure). Chromium memperlakukan cookie tanpa atribut sebagai Lax; 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. CSRFProtect dari Flask-WTF menerbitkannya lewat csrf_token() dan memeriksa field csrf_token atau header X-CSRFToken.
Fetch Metadata (Sec-Fetch-Site)
Browser modern mengirim Sec-Fetch-Site: cross-site, same-site, same-origin, atau none pada setiap request, dan halaman tidak bisa memalsukannya. Menolak metode tidak aman yang bukan same-origin memblokir CSRF dengan satu pemeriksaan di server.
Mengubah state lewat GET
Route yang mengubah data lewat GET bisa dipicu dengan tag <img> atau tautan, dan SameSite=Lax tetap mengirim cookie pada navigasi GET tingkat atas. Membaca request.values menerima query string selain body formulir.

Alur Serangan Langkah demi Langkah

Langkah 1

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.

Langkah 2

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.

Langkah 3

Browser mengirim request beserta cookie

Request membawa sesi korban, dan server tidak melihat apa pun yang menunjukkan bahwa formulir itu berasal dari situs lain.

Langkah 4

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

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

Sumber

← Lihat seluruh direktori keamanan Semua panduan kerentanan →