fix(security): supprimer le XSS stocke du calendrier
SEC-XSS-001. Chaine complete : un nom d'utilisateur libre arrivait dans le
DOM d'un coach ou d'un administrateur, en meme origine, avec sa session.
La CSP autorisant 'unsafe-inline', rien ne l'arretait.
Cote serveur - la cause
/matches/api/events construisait de la presentation dans un champ JSON :
match_desc = participants_str + f"<br>{match.description}"
Le navigateur deposait cette valeur telle quelle dans innerHTML. Les noms
de joueurs y transitaient sans echappement -- et il ne pouvait pas y en
avoir : c'est du JSON, pas du HTML.
Les deux valeurs etaient deja des cles distinctes du payload. La
concatenation faisait donc aussi afficher les participants deux fois dans
le modal : une fois dans "Teams", une fois en tete de "Description".
Corriger la faille corrige l'affichage.
Cote navigateur - le sink
showEventModal assemblait une chaine HTML puis l'affectait a innerHTML.
Remplace par une construction de noeuds : makeEl / detailItem /
multilineNode / teamNode passent tout texte par textContent. Les retours
a la ligne d'une description restent rendus, via des <br> crees en dur.
Les deux listes deroulantes concatenaient egalement titres de tryout et
noms d'equipe dans innerHTML. Remplacees par new Option(), dont le
premier argument est pose en texte.
Verification
5 tests sur le contrat de l'API, dont un avec un nom d'utilisateur
hostile ecrit directement en base -- ce que la validation refuse
desormais, mais que des lignes anterieures peuvent contenir.
Le JavaScript inline extrait passe `node --check`.
Reste ouvert : la CSP autorise toujours 'unsafe-inline' (SEC-WEB-001), donc
la defense en profondeur manque encore. Suivi en OPS-010.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -48,7 +48,12 @@ def api_events():
|
||||
for tryout in tryouts:
|
||||
for match in tryout.matches:
|
||||
match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b'
|
||||
match_desc = match.description or ''
|
||||
# 'description' used to be participants_str + '<br>' + description.
|
||||
# Building presentation markup inside a JSON field is what carried
|
||||
# the stored XSS: the browser dropped it straight into innerHTML,
|
||||
# and player usernames travelled through it unescaped. The two
|
||||
# values are already separate keys, so the concatenation also made
|
||||
# the modal show the participants twice.
|
||||
participants_str = ''
|
||||
if match.match_type == 'team_vs_team':
|
||||
teams = []
|
||||
@@ -56,14 +61,12 @@ def api_events():
|
||||
teams.append(match.team1.name)
|
||||
if match.team2:
|
||||
teams.append(match.team2.name)
|
||||
participants_str = f"{' vs '.join(teams)}"
|
||||
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
|
||||
participants_str = ' vs '.join(teams)
|
||||
else:
|
||||
player_names = []
|
||||
for p in match.participants.all():
|
||||
player_names.append(p.player.username if p.player else 'Unknown Player')
|
||||
participants_str = ', '.join(player_names) if player_names else 'No players'
|
||||
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
|
||||
|
||||
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
|
||||
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
||||
@@ -79,7 +82,7 @@ def api_events():
|
||||
'type': 'match', 'color': match_color,
|
||||
'extendedProps': {
|
||||
'location': match.location or tryout.location or 'TBD',
|
||||
'status': match.status, 'description': match_desc,
|
||||
'status': match.status, 'description': match.description or '',
|
||||
'match_type': match.match_type,
|
||||
'tryout_id': tryout.id, 'match_id': match.id,
|
||||
'start_time': start_time_str, 'end_time': end_time_str,
|
||||
|
||||
Reference in New Issue
Block a user