fix: bloqueia open-redirect via next=/\evil.com no login

_proximo_seguro() (app/routers/auth.py) so recusava next que comecasse
com "//", mas navegadores modernos tratam "/\" como equivalente a
"//" (URL protocol-relative), entao next=/\evil.com ainda era um open
redirect explorável via link de login malicioso.

Bloqueia tambem o prefixo "/\".

Teste de regressao em tests/test_router_auth.py: POST /login com
next=/\evil.com precisa redirecionar para /clientes, nao para o host
externo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-13 21:18:46 -03:00
co-authored by Claude Sonnet 5
parent aa1c528197
commit ba8ffaede7
2 changed files with 12 additions and 1 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ templates = Jinja2Templates(directory="app/templates")
def _proximo_seguro(next_: Optional[str]) -> str:
"""Só aceita um path interno (começa com "/", sem "//") — nunca uma URL
externa, para não virar um open redirect via POST /login?next=..."""
if next_ and next_.startswith("/") and not next_.startswith("//"):
if next_ and next_.startswith("/") and not next_.startswith(("//", "/\\")):
return next_
return "/clientes"
+11
View File
@@ -56,6 +56,17 @@ def test_post_login_next_externo_e_ignorado_por_seguranca():
assert resposta.headers["location"] == "/clientes"
def test_post_login_next_com_barra_invertida_e_ignorado_por_seguranca():
# "/\evil.com" é tratado por navegadores modernos como equivalente a
# "//evil.com" (URL protocol-relative) — precisa ser bloqueado também.
resposta = client.post(
"/login",
data={"usuario": "admin", "senha": "teste123", "next": "/\\evil.com", "csrf_token": CSRF_TESTE},
)
assert resposta.status_code == 303
assert resposta.headers["location"] == "/clientes"
def test_post_login_credenciais_erradas_nao_grava_cookie():
resposta = client.post(
"/login", data={"usuario": "admin", "senha": "errada", "csrf_token": CSRF_TESTE}