fix(auth): enumeration, verrou, deconnexion en GET, redirection ouverte

Quatre constats de la liste des gains rapides, tous sur auth.py.

SEC-017 -- enumeration de comptes
Le formulaire repondait "il vous reste 3 tentative(s)" a un compte connu et
"verifiez le nom d utilisateur et le mot de passe" a un inconnu. Le decompte
lui-meme etait la fuite : la meme information, etalee sur cinq requetes.
S y ajoutait un ecart de temps de reponse, check_password n etant appele que
si la ligne existait -- scrypt est cher, l ecart est mesurable.

Un seul message pour tous les echecs, et la verification s execute
desormais sur les deux branches : contre un hachage aleatoire tire une fois
par processus quand l identifiant n existe pas.

SEC-018 -- verrou de compte declenchable par un tiers
Cinq mauvaises reponses mettaient un compte connu hors service pendant
quinze minutes, indefiniment renouvelables. Sur un compte president, c est
toute l administration, et aucun ecran ne permettait de defaire.

Le compteur et la fenetre restent -- ce sont la trace qu un administrateur
lit quand un compte est pilonne, et la fenetre double jusqu a un plafond.
Ce qui change : de bons identifiants passent, fenetre ouverte ou non, et
remettent le compteur a zero. Le proprietaire du compte ne peut plus etre
bloque par un tiers.

Ce que cela coute, dit franchement : un verrou dur n arretait de toute
facon pas un attaquant ayant trouve le mot de passe -- il lui suffisait
d attendre. Le debit de tentatives reste borne par la limite de 10/minute
par IP. Une limite par couple (compte, IP) demanderait un stockage dedie ;
elle attend Alembic.

L evenement account.locked devient account.throttled : "locked" affirmait
plus que ce qui se passe.

SEC-019 -- deconnexion en GET
/auth/logout n avait pas de methods, donc GET, donc hors protection CSRF :
n importe quelle page pouvait deconnecter un visiteur avec une balise img.
La route passe en POST et l entree de navigation devient un formulaire avec
jeton. Le style suit -- les regles .nav-links visaient les liens seuls.

SEC-020 -- validation de redirection
is_safe_url interrogeait urlparse().netloc. urlparse lit /\evil.com comme
un chemin, sans netloc ; plusieurs navigateurs normalisent l antislash en
barre oblique avant de resoudre, ce qui en fait //evil.com. La fonction
refuse maintenant antislash et caracteres de controle, exige un chemin
enracine, et compare l origine explicitement.

Le xfail(strict) qui documentait SEC-017 est leve. 40 tests dans
test_auth_session.py, dont la table des cibles refusees.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-08 15:05:13 -04:00
co-authored by Claude Opus 5
parent d85da32ef5
commit 835a394d3f
11 changed files with 1245 additions and 720 deletions
+17 -3
View File
@@ -138,7 +138,10 @@ a:hover { color: var(--primary-dark); }
padding: 10px 0;
}
.nav-links li a {
/* Logging out is a POST, so its nav entry is a button inside a form
rather than a link. It has to read as one of the entries above it. */
.nav-links li a,
.nav-links li .nav-form button {
display: flex;
align-items: center;
gap: 12px;
@@ -148,7 +151,17 @@ a:hover { color: var(--primary-dark); }
font-size: 0.9rem;
}
.nav-links li a:hover, .nav-links li a.active {
.nav-links li .nav-form button {
width: 100%;
background: none;
border: 0;
font-family: inherit;
text-align: left;
cursor: pointer;
}
.nav-links li a:hover, .nav-links li a.active,
.nav-links li .nav-form button:hover {
background: rgba(255,255,255,0.08);
color: white;
}
@@ -158,7 +171,8 @@ a:hover { color: var(--primary-dark); }
padding-left: 17px;
}
.nav-links li a i { width: 20px; text-align: center; font-size: 1.1rem; }
.nav-links li a i,
.nav-links li .nav-form button i { width: 20px; text-align: center; font-size: 1.1rem; }
.nav-divider {
height: 1px;