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}