OAuth 2.0: Serangan redirect_uri dan login CSRF
Pemeriksaan redirect URI yang longgar pada server otorisasi atau open redirect di callback terdaftar dapat mengekspos kode otorisasi. Eksploitasi tetap bergantung pada pengikatan kode dan pemeriksaan token endpoint; pengikatan alur login yang hilang juga dapat menyebabkan login CSRF.
💡 Penjelasan sederhana
Bayangkan server otorisasi sebagai kantor pos tepercaya: kantor itu hanya mengirim kode sekali pakai ke alamat balasan yang didaftarkan aplikasi. Jika pencocokan alamat terlalu longgar atau endpoint terdaftar meneruskan pengunjung ke situs sembarang, kode dapat dialihkan. Kode yang tertangkap tidak otomatis memberi akses: penyerang juga harus memenuhi persyaratan pertukaran token, termasuk PKCE dan autentikasi klien jika diwajibkan. Masalah login CSRF yang terpisah terjadi ketika situs tidak dapat memastikan bahwa browser memulai login yang sedang diselesaikannya.
Konsep inti dan istilah penting
- Authorization Code
- Kredensial berumur pendek yang dikembalikan melalui redirect browser. Klien menukarnya di token endpoint; kode terikat pada klien dan redirect URI, serta pada verifier jika PKCE digunakan.
redirect_uriharus cocok persis- RFC 9700 mewajibkan server otorisasi membandingkan redirect URI yang diminta dengan URI yang telah didaftarkan secara persis sebagai string. Ada pengecualian terbatas untuk port dinamis localhost pada redirect aplikasi native.
statedan login CSRF- Nilai tak terduga yang terikat pada sesi browser dan diperiksa saat callback dapat memastikan respons terkait dengan login yang dimulai pengguna. PKCE yang diwajibkan juga dapat melindungi dari CSRF; menggunakan keduanya memberi pertahanan berlapis.
- PKCE (Proof Key for Code Exchange)
- Klien membuat verifier rahasia lalu mengirim challenge turunannya dalam permintaan otorisasi. Token endpoint mewajibkan verifier untuk menukar kode sehingga risiko penyadapan kode berkurang pada klien publik maupun rahasia.
Tahapan serangan redirect dan login CSRF
Server otorisasi menerima redirect URI yang tidak aman
Server yang salah menggunakan wildcard, aturan prefix, atau substring alih-alih membandingkan redirect_uri yang diminta dengan nilai terdaftar secara persis. RFC 9700 mewajibkan pencocokan persis kecuali pengecualian port localhost aplikasi native yang ditentukan.
Endpoint terdaftar meneruskan ke situs yang tidak tepercaya
URI yang diterima dapat mengarah ke open redirect yang dikendalikan klien, misalnya https://app.example/redirect?next=.... Ini menggabungkan pemeriksaan server otorisasi yang longgar dengan kelemahan CWE-601 tersendiri pada aplikasi.
Browser membawa kode melewati redirect
Setelah pengguna menyetujui, server mengirim kode ke URI yang diterima. Jika open redirect meneruskan permintaan dan mempertahankan kode, penyerang dapat menangkapnya. Kode saja bukan bearer access token.
Penukaran token tetap harus lolos pemeriksaan
Penyerang hanya dapat memakai kode tertangkap jika memenuhi pemeriksaan token endpoint. PKCE mencegah penukaran tanpa verifier asli; autentikasi klien rahasia dan pencocokan redirect URI memberikan pengikatan tambahan.
Tanpa pengikatan respons, login CSRF dapat terjadi
Jika callback menerima respons otorisasi yang tidak terkait dengan upaya login browser, penyerang dapat membuat korban menyelesaikan alur yang terkait dengan akun penyerang. Ini adalah kebingungan akun, bukan akses otomatis ke akun korban.
Kode sumber: pola rentan dan implementasi aman
# VULNERABLE: user input is embedded in redirect_uri; state and PKCE are absent.
from urllib.parse import urlencode
from flask import Flask, request, redirect
app = Flask(__name__)
AUTHORIZATION_ENDPOINT = "https://oauth-provider.example/authorize"
@app.route("/auth/login")
def oauth_login():
next_url = request.args.get("next", "")
# The callback is an open redirect, and this URI varies with user input.
redirect_uri = "https://app.example/redirect?next=" + next_url
params = {
"client_id": "example-client",
"response_type": "code",
"redirect_uri": redirect_uri,
}
# Exploitation also requires the authorization server to accept this URI
# under an unsafe wildcard or prefix rule.
return redirect(AUTHORIZATION_ENDPOINT + "?" + urlencode(params))
@app.route("/auth/callback")
def oauth_callback():
# No state check ties this response to the browser's login attempt.
code = request.args.get("code")
return exchange_code_for_tokens(code) # no PKCE verifier provided
# SECURE: fixed registered redirect URI, session-bound state, and PKCE.
import base64
import hashlib
import secrets
from urllib.parse import urlencode
from flask import Flask, request, redirect, session, abort
app = Flask(__name__)
AUTHORIZATION_ENDPOINT = "https://oauth-provider.example/authorize"
REDIRECT_URI = "https://app.example/auth/callback" # exact registered value
CLIENT_ID = "example-client"
@app.route("/auth/login")
def oauth_login():
state = secrets.token_urlsafe(32)
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode("ascii")).digest()
).rstrip(b"=").decode("ascii")
session["oauth_state"] = state
session["pkce_verifier"] = verifier
params = {
"client_id": CLIENT_ID,
"response_type": "code",
"redirect_uri": REDIRECT_URI,
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
}
return redirect(AUTHORIZATION_ENDPOINT + "?" + urlencode(params))
@app.route("/auth/callback")
def oauth_callback():
expected = session.pop("oauth_state", None)
if not expected or not secrets.compare_digest(
request.args.get("state", ""), expected
):
abort(403, "Invalid OAuth state")
code = request.args.get("code")
verifier = session.pop("pkce_verifier", None)
if not code or not verifier:
abort(400, "Missing authorization response")
# The token request must use this exact redirect_uri and verifier.
tokens = exchange_code_for_tokens(
code=code, redirect_uri=REDIRECT_URI, code_verifier=verifier
)
session["user_id"] = tokens["user_id"]
return redirect("/dashboard")
Daftar periksa penguatan untuk engineering
- Atur server otorisasi agar membandingkan
redirect_uridengan URI yang telah didaftarkan secara persis sebagai string. Pertahankan hanya pengecualian port localhost aplikasi native dari RFC 9700. - Jangan masukkan tujuan yang dikendalikan pengguna ke URI OAuth callback. Hapus open redirect dari jalur callback dan terima hanya ID rute lokal yang ada di allowlist.
- Gunakan Authorization Code Flow dengan PKCE S256 untuk semua jenis klien. Kirim
redirect_uriterdaftar yang sama pada permintaan otorisasi dan token. - Ikat callback ke upaya login: validasi
statesekali pakai yang terikat sesi, atau andalkan PKCE untuk perlindungan CSRF hanya jika server otorisasi mewajibkan dukungan PKCE. Menggunakan keduanya juga merupakan pertahanan berlapis. - Kode otorisasi yang tertangkap tidak cukup dengan sendirinya. Pastikan penukaran token mensyaratkan autentikasi klien yang benar dan PKCE verifier; jangan pernah menaruh access token di URL redirect.
- Jangan catat kode otorisasi dalam log dan tetapkan
Referrer-Policyyang ketat pada halaman callback. Jangan tampilkan konten pihak ketiga sebelum parameter kueri sensitif dihapus.