FlawOpen
● CWE-601 / CWE-352

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_uri harus 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.
state dan 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

Path A · 1

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.

Path A · 2

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.

Path A · 3

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.

Path A · 4

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.

Path B

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

Pola rentan
# 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
Pertahanan yang diperkuat
# 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

← Jelajahi Direktori Keamanan Lengkap Lihat Playbook Kerentanan → RFC 9700: Praktik Terbaik Keamanan OAuth 2.0 → RFC 7636: Proof Key for Code Exchange (PKCE) → RFC 6749: Manipulasi URI Pengalihan Authorization Code → CWE-601: Open Redirect → CWE-352: Pemalsuan Permintaan Lintas Situs →