Files
GTI_GERENCIADOR_FISCAL/app/routers/auth.py
T
GustavoandClaude Sonnet 5 ba8ffaede7 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>
2026-08-13 21:18:46 -03:00

63 lines
2.3 KiB
Python

"""
Rotas de login/logoff. O painel não usa mais HTTP Basic Auth (ver
app/auth.py) — quem não está logado é redirecionado para GET /login pelo
exception handler de NaoAutenticado (registrado em app/main.py).
"""
from typing import Optional
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import session
from ..auth import UsuarioLogado, autenticar_credenciais, exigir_login
from ..csrf import exigir_csrf, token_do_contexto
router = APIRouter()
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(("//", "/\\")):
return next_
return "/clientes"
@router.get("/login", response_class=HTMLResponse)
def form_login(request: Request, next: Optional[str] = None):
return templates.TemplateResponse(
request, "login.html", {"csrf_token": token_do_contexto(request), "next": next or ""}
)
@router.post("/login", dependencies=[Depends(exigir_csrf)])
def login(
request: Request,
usuario: str = Form(...),
senha: str = Form(...),
next: Optional[str] = Form(None),
):
usuario_logado = autenticar_credenciais(usuario, senha)
if usuario_logado is None:
return templates.TemplateResponse(
request,
"login.html",
{"csrf_token": token_do_contexto(request), "next": next or "", "erro": "Usuário ou senha inválidos."},
)
destino = _proximo_seguro(next)
resposta = RedirectResponse(url=destino, status_code=303)
token = session.criar_token(usuario_logado.login, usuario_logado.nome, usuario_logado.perfil)
resposta.set_cookie(session.SESSION_COOKIE_NAME, token, httponly=True, samesite="lax")
return resposta
@router.post("/logout", dependencies=[Depends(exigir_csrf)])
def logout(usuario: UsuarioLogado = Depends(exigir_login)):
resposta = RedirectResponse(url="/login", status_code=303)
resposta.delete_cookie(session.SESSION_COOKIE_NAME)
return resposta