From ba8ffaede7292311627816fac922d2370ff1ffd1 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 13 Aug 2026 21:18:46 -0300 Subject: [PATCH] fix: bloqueia open-redirect via next=/\evil.com no login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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 --- app/routers/auth.py | 2 +- tests/test_router_auth.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index 397c3be..481417d 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -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" diff --git a/tests/test_router_auth.py b/tests/test_router_auth.py index 19c6197..d522d8f 100644 --- a/tests/test_router_auth.py +++ b/tests/test_router_auth.py @@ -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}