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:
+13
-9
@@ -103,6 +103,19 @@ def create_app(config=None):
|
||||
allowed_origins = str(app.config.get('CORS_ALLOWED_ORIGINS') or '').split(',')
|
||||
allowed_origins = [origin.strip() for origin in allowed_origins if origin.strip()]
|
||||
|
||||
# CORS is only configured when origins are named explicitly.
|
||||
#
|
||||
# The previous else-branch called CORS(app, supports_credentials=True)
|
||||
# with no origins argument. flask-cors then defaults to '*' and, because
|
||||
# credentials are allowed, echoes back whatever Origin the caller sent
|
||||
# together with Access-Control-Allow-Credentials: true — the opposite of
|
||||
# the "allow all (development) or none (production)" the comment claimed.
|
||||
#
|
||||
# Exploitation was blocked by SESSION_COOKIE_SAMESITE = 'Lax', which stops
|
||||
# the browser attaching the session cookie to a cross-site fetch. That is
|
||||
# a single setting standing between a misconfiguration and a cross-origin
|
||||
# data leak. This application renders server-side HTML on one origin and
|
||||
# needs no CORS policy at all.
|
||||
if allowed_origins:
|
||||
CORS(
|
||||
app,
|
||||
@@ -111,15 +124,6 @@ def create_app(config=None):
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
max_age=3600, # Cache preflight for 1 hour
|
||||
)
|
||||
else:
|
||||
# When no origins specified, allow all (development) or none (production)
|
||||
# In production with a reverse proxy, CORS is handled at the Nginx level
|
||||
CORS(
|
||||
app,
|
||||
supports_credentials=True,
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
max_age=3600,
|
||||
)
|
||||
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
Reference in New Issue
Block a user