fix(security): proteger le dernier administrateur, et fermer le CORS permissif
SEC-AUTHZ-007 - auto-verrouillage de l'administration
Le changement de role n'excluait ni l'utilisateur courant, ni le dernier
compte admin actif. Une seule manipulation suffisait a transformer le seul
president en joueur, et plus aucune interface ne permettait de revenir en
arriere : il fallait une intervention directe en base.
Deux gardes distinctes, parce que ce sont deux erreurs differentes :
- changer son propre role est refuse, meme s'il reste d'autres admins.
Un president qui veut se retrograder doit le faire faire par un autre.
- retrograder le dernier admin actif est refuse.
Le decompte exclut les comptes desactives : trois admins dont deux
desactives, cela fait un seul administrateur reel.
SEC-WEB-003 - CORS ouvert par defaut
Sans CORS_ALLOWED_ORIGINS, la branche else appelait
CORS(app, supports_credentials=True) sans argument origins. flask-cors
retient alors '*' et, les identifiants etant autorises, renvoie en echo
l'Origin de l'appelant avec Access-Control-Allow-Credentials: true --
l'inverse exact de ce qu'annonçait le commentaire.
L'exploitation etait bloquee par SESSION_COOKIE_SAMESITE = 'Lax', qui
empeche le navigateur de joindre le cookie de session a une requete
fetch inter-site. Toute la protection tenait donc a ce seul reglage.
Cette application rend du HTML en meme origine : elle n'a besoin
d'aucune politique CORS. La branche par defaut est supprimee, la
configuration explicite reste possible.
5 tests ajoutes, dont deux verifient que les chemins legitimes continuent
de fonctionner : un autre administrateur reste retrogradable, et une
origine explicitement configuree est toujours honoree.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -166,6 +166,25 @@ def edit_user(user_id):
|
||||
flash('Email already in use by another account.', 'danger')
|
||||
return _rerender()
|
||||
|
||||
if user.role != role:
|
||||
# Two ways to lock everyone out of administration, neither of
|
||||
# which any interface can undo afterwards.
|
||||
if user.id == actor_id:
|
||||
flash('You cannot change your own role. Ask another president '
|
||||
'to do it.', 'danger')
|
||||
return _rerender()
|
||||
|
||||
if user.role == 'admin':
|
||||
remaining_admins = User.query.filter(
|
||||
User.role == 'admin',
|
||||
User.is_active_account.is_(True),
|
||||
User.id != user.id,
|
||||
).count()
|
||||
if remaining_admins == 0:
|
||||
flash('This is the last active president. Promote another '
|
||||
'account before changing this one.', 'danger')
|
||||
return _rerender()
|
||||
|
||||
# Change role via raw SQL to avoid polymorphic identity corruption.
|
||||
# Must discard the entire session because the polymorphic discriminator
|
||||
# change invalidates the identity map for this instance and anything
|
||||
|
||||
Reference in New Issue
Block a user