fix(authz): teams.py etait le module que la validation n avait pas atteint
SEC-16, de l'audit anterieur. La vague G a pose un schema a la frontiere de
matches, team_matches, tryouts et evaluations, et le HANDOFF en a tire une
regle : tout champ de formulaire passe par un schema de app/validators.py,
pas par request.form.get. teams.py ne l'avait jamais appliquee.
Deux defauts, pas un.
int(request.form.get('coach_id')) leve sur une valeur non numerique : une
soumission fabriquee etait un 500.
Et l'identifiant obtenu etait ensuite resolu sans verifier le role du compte,
dans deux des trois endroits qui le faisaient. La branche sync_staff
d'edit_team testait isinstance(user, Coach) ; son autre branche non, et
create_team non plus. Le meme fichier en desaccord avec lui-meme, sur
exactement le defaut que la vague G avait corrige dans tryouts.py -- une
soumission fabriquee pouvait nommer un joueur parmi les coachs d'une equipe.
L'identifiant vient d'un <select> rendu par le navigateur : c'est une valeur
que le client choisit.
_staff_member est la reponse unique, et OrgTeamSchema garantit que les
identifiants arrivent en entiers. Les deux tests qui epinglent le role
tombent si la verification saute : verifie par mutation.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -716,6 +716,49 @@ class TryoutSchema(StripMixin):
|
||||
)
|
||||
|
||||
|
||||
class OrgTeamSchema(StripMixin):
|
||||
"""An organisation team, created or edited (SEC-16).
|
||||
|
||||
`teams.py` was the one route module wave G's validation pass did not
|
||||
reach, and it still read every field through `request.form.get` and
|
||||
converted with a bare `int()`. Two consequences, both reachable from a
|
||||
hand-made POST by anyone allowed to manage teams:
|
||||
|
||||
- `int('abc')` raises, so a non-numeric `coach_id` was a 500;
|
||||
- `int('-1')` was accepted, and the id was then looked up without ever
|
||||
checking what role the account had.
|
||||
|
||||
The second is the same defect wave G found in `tryouts.py`, where a
|
||||
forged submission could name a *player* as coach. Here it survived in
|
||||
two of the three branches of the same file: `edit_team`'s `sync_staff`
|
||||
path checks `isinstance(user, Coach)`, its other path does not, and
|
||||
`create_team` does not either. Role checking belongs with the lookup,
|
||||
not with the schema, so it lives in `_staff_member` in the route — but
|
||||
the ids have to survive the trip as integers first.
|
||||
"""
|
||||
|
||||
name = fields.String(
|
||||
required=True,
|
||||
validate=validate.Length(min=1, max=100, error=_l('Team name is required.')),
|
||||
error_messages={'required': _l('Team name is required.')},
|
||||
)
|
||||
coach_id = fields.Integer(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Range(min=1),
|
||||
error_messages={'invalid': _l('Invalid coach selection.')},
|
||||
)
|
||||
manager_id = fields.Integer(
|
||||
allow_none=True,
|
||||
load_default=None,
|
||||
validate=validate.Range(min=1),
|
||||
error_messages={'invalid': _l('Invalid manager selection.')},
|
||||
)
|
||||
coach_ids = fields.List(fields.Integer(validate=validate.Range(min=1)), load_default=list)
|
||||
manager_ids = fields.List(fields.Integer(validate=validate.Range(min=1)), load_default=list)
|
||||
sync_staff = fields.String(allow_none=True, load_default=None)
|
||||
|
||||
|
||||
def score_field():
|
||||
"""One evaluation criterion: 1 to 10, or not scored at all."""
|
||||
return fields.Integer(
|
||||
|
||||
Reference in New Issue
Block a user