FlawOpen
● Redirecionamento e CSRF / CWE-601, CWE-352

OAuth 2.0: ataques a redirect_uri, roubo de código e login CSRF

Uma validação permissiva de redirecionamento no servidor de autorização ou um redirecionamento aberto em um callback registrado pode expor um código de autorização. A exploração ainda depende dos controles de vínculo do código e do endpoint de tokens; a falta de vínculo do fluxo de login também pode causar login CSRF.

💡 Explicação em linguagem simples (ELI5)

Imagine o servidor de autorização como uma agência dos Correios confiável: ela envia um código de uso único somente ao endereço de retorno registrado para o aplicativo. Se esse endereço for comparado de forma flexível, ou o endpoint registrado encaminhar visitantes a qualquer site, o código poderá ser desviado. Capturar o código não concede acesso automaticamente: o atacante também precisa satisfazer os requisitos de troca de token do cliente, incluindo PKCE e autenticação do cliente quando exigida. Uma falha separada de login CSRF ocorre quando o site não consegue confirmar se o navegador iniciou o login que está concluindo.

Conceitos principais e termos-chave

Código de autorização
Credencial de curta duração devolvida pelo redirecionamento do navegador. O cliente a troca no endpoint de tokens; o resgate é vinculado ao cliente e ao URI de redirecionamento e, quando usado, ao verificador PKCE.
Correspondência exata de redirect_uri
A RFC 9700 exige que o servidor de autorização compare o URI solicitado com um URI pré-registrado usando correspondência exata de string, com uma exceção restrita para porta dinâmica em localhost de aplicativos nativos.
state e login CSRF
Um valor imprevisível vinculado à sessão do navegador e verificado no callback pode associar a resposta a um login iniciado pelo usuário. PKCE aplicado também pode proteger contra CSRF; usar ambos adiciona defesa em profundidade.
PKCE (Proof Key for Code Exchange)
O cliente cria um verificador secreto e envia seu desafio derivado na solicitação de autorização. O endpoint de tokens exige o verificador para resgatar o código, reduzindo a interceptação para clientes públicos e confidenciais.

Fluxos de ataque por redirecionamento e login CSRF

Caminho A · 1

O servidor de autorização aceita um URI inseguro

Um servidor de autorização defeituoso usa wildcard, prefixo ou substring em vez de comparar o redirect_uri solicitado com um valor registrado exato. A RFC 9700 exige correspondência exata, salvo a exceção de porta localhost para aplicativos nativos.

Caminho A · 2

Um endpoint registrado encaminha para site não confiável

O URI aceito pode chegar a um redirecionamento aberto controlado pelo cliente, como https://app.example/redirect?next=.... Isso combina uma validação fraca do servidor de autorização com uma fraqueza CWE-601 separada na aplicação cliente.

Caminho A · 3

O navegador leva o código pelo redirecionamento

Após a autorização do usuário, o servidor devolve o código ao URI aceito. Se o redirecionamento aberto encaminhar a solicitação preservando o código, o atacante poderá capturá-lo. O código, sozinho, não é um token de acesso bearer.

Caminho A · 4

A troca por token ainda precisa funcionar

O atacante só pode usar o código capturado se satisfizer as verificações do endpoint de tokens. PKCE impede o resgate sem o verificador original; a autenticação do cliente confidencial e a correspondência do URI acrescentam outros vínculos.

Caminho B

A falta de vínculo da resposta permite login CSRF

Se o callback aceita uma resposta de autorização que não foi vinculada à tentativa de login do navegador, o atacante pode fazer a vítima concluir um fluxo associado à conta do atacante. Isso causa confusão de conta, não acesso automático à conta da vítima.

Código-fonte: falha e implementação segura

PADRÃO VULNERÁVEL
# 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
DEFESA REFORÇADA
# 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")

Lista de verificação de proteção de engenharia

← Navegar pelo diretório completo de segurança Explorar guias de vulnerabilidades → RFC 9700: melhores práticas de segurança OAuth 2.0 → RFC 7636: Proof Key for Code Exchange (PKCE) → RFC 6749: manipulação do URI de redirecionamento do código de autorização → CWE-601: redirecionamento aberto → CWE-352: falsificação de solicitação entre sites →