diff --git a/CHANGELOG.md b/CHANGELOG.md index 8635abb..27ebc4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH** --- +## [1.32.4] — 2026-06-09 + +### Fixed +- **Login não acontecia: o botão "recarregava" a tela de login (regressão da v1.31.0).** Após o logout, ao logar com credenciais corretas e sem `?next=`, a sessão era criada mas o usuário voltava para `/login` (ou caía numa página "Redirecting… target URL:" em branco) — sem mensagem de erro, dando a impressão de senha errada; remover o `/login` da URL revelava que já estava logado. Causa: o fix de open redirect da 1.31.0 passou a validar o destino só por `urlparse().netloc`/`.scheme`, e a string vazia (login sem `next`) passava nesse teste e virava `redirect("")` — que o navegador resolve recarregando a própria `/login`. Agora o redirect pós-login só segue o `next` quando é um caminho relativo de verdade (começa com `/` e não `//`); sem `next`, vai para o dashboard. A barreira anti-open-redirect do CodeQL é preservada. Teste de regressão cobrindo o login sem `next`. + ## [1.32.3] — 2026-06-09 ### Changed diff --git a/VERSION b/VERSION index 00b2252..ef62e36 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.32.3 +1.32.4 diff --git a/app.py b/app.py index 21bf6e3..ae7d88b 100644 --- a/app.py +++ b/app.py @@ -658,8 +658,12 @@ def _promote_session(user, remember, ip, next_url=""): # EXATAMENTE o snippet recomendado pelo CodeQL para py/url-redirection: # navegadores tratam "\" como "/", então remove as barras invertidas e checa # `urlparse(target).netloc`/`.scheme` inline — caminho relativo é seguro. + # Exige um caminho relativo de verdade (começa com "/" mas não "//"): sem + # `next`, target="" passaria no teste de netloc/scheme e cairia num + # `redirect("")`, que recarrega a própria /login (regressão da v1.31.0). target = (next_url or "").replace("\\", "") - if not urlparse(target).netloc and not urlparse(target).scheme: + if target.startswith("/") and not target.startswith("//") \ + and not urlparse(target).netloc and not urlparse(target).scheme: return redirect(target) return redirect(url_for("dashboard")) diff --git a/tests/test_security.py b/tests/test_security.py index 5ad4b67..9b89b6e 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -76,6 +76,17 @@ def test_login_honors_local_next(client): assert resp.headers["Location"].endswith("/spools") +def test_login_without_next_goes_to_dashboard(client): + """Login normal (sem ?next=) NÃO pode cair num redirect("") — que recarrega + a própria /login (regressão da v1.31.0). Tem que ir pro dashboard.""" + resp = client.post("/login", + data={"username": ADMIN_USER, "password": ADMIN_PASS}) + assert resp.status_code == 302 + loc = resp.headers["Location"] + assert loc not in ("", "/login") + assert not loc.endswith("/login") + + # ── Troca de senha forçada (rec 5) ─────────────────────────────────────────── def test_admin_bootstrap_requires_password_change(db):