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]>
ARCH-008. `role` est le discriminateur polymorphe, et SQLAlchemy fixe la
classe d une instance au chargement. edit_user ecrivait donc la colonne par
un UPDATE de niveau instruction puis relisait la ligne -- c est correct.
La suite ne l etait pas.
db.session.commit()
db.session.remove() # discard stale session entirely
user = User.query.get(user_id_local)
Deux defauts dans ces trois lignes.
1. remove() jette la session entiere. Tout ce que la requete tenait encore
se retrouvait detache, current_user compris ; le moindre acces a un
attribut ensuite levait DetachedInstanceError. La route ne survivait
qu en ayant recopie le nom et l id de l acteur dans des variables
locales avant -- un contournement, pas la correction. Un expunge de la
seule instance perimee suffit.
2. Le commit intermediaire coupait l edition en deux. Le role etait acquis
avant que le reste du formulaire soit applique : une erreur ensuite
laissait un compte promu et le reste perdu, sans qu aucune interface ne
le signale. Sans ce commit, l UPDATE reste dans la transaction, la
relecture le voit, et l ensemble part en un seul commit.
Les evenements d audit passent apres le commit. account.role_changed etait
journalise avant l UPDATE : le journal affirmait un changement que la
transaction pouvait encore annuler.
tests/test_role_change.py, 6 tests. Un seul echoue sur le code d avant --
celui de l atomicite ; les cinq autres fixent le comportement qui marchait
deja, pour que la suite de la vague D ne le casse pas.
Co-Authored-By: Claude Opus 5 <[email protected]>