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.
statee 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
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.
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.
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.
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.
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
# 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")
Lista de verificação de proteção de engenharia
- Configure o servidor de autorização para comparar
redirect_uricom um URI pré-registrado usando correspondência exata de string. Mantenha apenas a exceção da RFC 9700 para portas localhost de aplicativos nativos. - Não inclua um destino controlado pelo usuário no URI de callback OAuth. Remova redirecionamentos abertos dos callbacks e aceite somente identificadores locais de rota em uma lista de permissões.
- Use o fluxo de código de autorização com PKCE S256 em todos os tipos de cliente. Envie o mesmo
redirect_uriregistrado na solicitação de autorização e na de token. - Vincule o callback à tentativa de login: valide um
stateúnico e vinculado à sessão ou confie no PKCE para CSRF somente quando o suporte do servidor de autorização estiver garantido. Usar ambos é defesa em profundidade. - Um código capturado não basta. Exija também a autenticação correta do cliente e o verificador PKCE no resgate do token; nunca coloque tokens de acesso em URLs de redirecionamento.
- Evite registrar códigos de autorização em logs e defina uma
Referrer-Policyrestritiva nas páginas de callback; não renderize conteúdo de terceiros antes de remover parâmetros sensíveis da URL.