feat(csp): retirer unsafe-inline de script-src
SEC-WEB-001 / OPS-010, ferme. C'est cette directive qui laissait s'executer
le XSS stocke de SEC-XSS-001 au lieu de le bloquer.
Les cinq derniers gabarits sont migres : match_form 13, calendar 11,
teams 11, evaluate_player 9, view_tryout 8. Total sur le chantier : 82
gestionnaires en ligne retires dans 17 gabarits. Il n'en reste aucun.
Deux motifs generiques de plus dans main.js
data-mirror affichage direct de la valeur d'un curseur.
evaluate_player repetait le meme
oninput="this.nextElementSibling.textContent = ..."
sur ses neuf curseurs de note.
data-submit-on-change remplace onchange="this.form.submit()"
Markup genere dans des chaines JavaScript
match_form construisait sept gestionnaires par concatenation, en y
injectant l'identifiant du joueur. Le markup portait deja data-player-id :
returnToPool et assignToTeam lisent desormais leurs arguments depuis
l'element clique. Cela supprime a la fois l'attribut en ligne et la
concatenation qui l'alimentait. Meme motif que dans coach_availability.
Bascule
CSP_ALLOW_INLINE_SCRIPT passe a false. script-src vaut maintenant
'self' 'nonce-<aleatoire par requete>' https://cdn.jsdelivr.net.
La variable d'environnement reste, comme issue de secours si un
deploiement rencontrait un gestionnaire oublie -- mais la laisser active
revient a renoncer a la protection.
Le cliquet devient une garde
Le budget par gabarit est vide et les tests deviennent absolus : aucun
gestionnaire en ligne, et tout bloc <script> inline doit porter son
nonce. Sans nonce, un bloc n'est simplement pas execute, et rien dans les
journaux ne le signale -- d'ou le test.
Verifications
22 pages parcourues avec les trois roles : toutes rendent en 200, aucune
ne contient de gestionnaire en ligne, et chaque bloc inline porte bien le
nonce de sa propre reponse. Syntaxe JavaScript de chaque gabarit verifiee
par node --check.
193 tests. Le dernier xfail de SEC-WEB-001 reussissait, le marqueur est
retire. Il n'en reste qu'un : SEC-AUTH-006, enumeration de comptes.
style-src conserve 'unsafe-inline' : les attributs style="" sont partout et
ne sont pas un vecteur XSS a eux seuls. Migration distincte, non prioritaire.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
{% if can_edit %}
|
||||
<form method="POST" action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="status" onchange="this.form.submit()" class="form-select">
|
||||
<select name="status" data-submit-on-change class="form-select">
|
||||
<option value="upcoming" {% if tryout.status == 'upcoming' %}selected{% endif %}>Upcoming</option>
|
||||
<option value="in_progress" {% if tryout.status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="completed" {% if tryout.status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
@@ -30,7 +30,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<form method="POST" action="{{ url_for('tryouts.delete_tryout', tryout_id=tryout.id) }}" class="inline-form" onsubmit="return confirm('Are you sure you want to delete this entire tryout? This will remove all matches, teams, registrations, and evaluations.');">
|
||||
<form method="POST" action="{{ url_for('tryouts.delete_tryout', tryout_id=tryout.id) }}" class="inline-form" data-confirm="{{ _('Delete this entire tryout? This removes all its matches, teams, registrations and evaluations.') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete Tryout
|
||||
@@ -169,7 +169,7 @@
|
||||
{% if can_edit %}
|
||||
<form method="POST" action="{{ url_for('tryouts.update_registration_status', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="status" onchange="this.form.submit()" class="form-select form-select-sm">
|
||||
<select name="status" data-submit-on-change class="form-select form-select-sm">
|
||||
<option value="registered" {% if reg.status == 'registered' %}selected{% endif %}>Registered</option>
|
||||
<option value="attended" {% if reg.status == 'attended' %}selected{% endif %}>Attended</option>
|
||||
<option value="no_show" {% if reg.status == 'no_show' %}selected{% endif %}>No Show</option>
|
||||
@@ -199,7 +199,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not tryout.is_ended %}
|
||||
<form method="POST" action="{{ url_for('tryouts.remove_player', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form" onsubmit="return confirm('Remove {{ p.username }} from this tryout? This will also remove them from all teams and matches within this tryout.');">
|
||||
<form method="POST" action="{{ url_for('tryouts.remove_player', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form" data-confirm="{{ _('Remove %(name)s from this tryout? They will also be removed from every team and match within it.', name=p.username) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger" title="Remove player from tryout">
|
||||
<i class="fas fa-user-minus"></i> Remove
|
||||
@@ -226,7 +226,7 @@
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users-cog"></i> Teams</h3>
|
||||
{% if can_edit and not tryout.is_ended %}
|
||||
<button class="btn btn-sm btn-primary" onclick="showCreateTeam()">
|
||||
<button class="btn btn-sm btn-primary" data-action="show-create-team">
|
||||
<i class="fas fa-plus"></i> New Team
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -238,7 +238,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="text" name="team_name" placeholder="Team name" required class="form-input mr-2">
|
||||
<button type="submit" class="btn btn-sm btn-success">Create</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="hideCreateTeam()">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-action="hide-create-team">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -368,7 +368,7 @@
|
||||
{% for pp in item.player_presence %}
|
||||
<button class="btn btn-xs presence-toggle-btn {% if pp.attendance_confirmed %}presence-confirmed-btn{% else %}presence-pending-btn{% endif %}"
|
||||
title="{{ pp.player_name }}"
|
||||
onclick="toggleTryoutPresence({{ m.id }}, {{ pp.participant_id }}, this)">
|
||||
data-action="toggle-tryout-presence" data-match-id="{{ m.id }}" data-participant-id="{{ pp.participant_id }}">
|
||||
{{ pp.player_name[:2] | upper }} {% if pp.attendance_confirmed %}✅{% else %}⏳{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@@ -381,7 +381,7 @@
|
||||
<div class="presence-players" style="margin-top:6px;">
|
||||
<button class="btn btn-xs presence-toggle-btn {% if pp.attendance_confirmed %}presence-confirmed-btn{% else %}presence-pending-btn{% endif %}"
|
||||
title="Toggle your attendance"
|
||||
onclick="toggleTryoutPresence({{ m.id }}, {{ pp.participant_id }}, this)">
|
||||
data-action="toggle-tryout-presence" data-match-id="{{ m.id }}" data-participant-id="{{ pp.participant_id }}">
|
||||
Me {% if pp.attendance_confirmed %}✅{% else %}⏳{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
@@ -614,5 +614,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
miniCalendar.render();
|
||||
}
|
||||
});
|
||||
|
||||
// Behaviours declared in the markup, dispatched by the delegated listener
|
||||
// in main.js. Inline onclick attributes cannot be authorised by a CSP nonce.
|
||||
registerActions({
|
||||
'show-create-team': showCreateTeam,
|
||||
'hide-create-team': hideCreateTeam,
|
||||
'toggle-tryout-presence': function (element) {
|
||||
toggleTryoutPresence(
|
||||
element.getAttribute('data-match-id'),
|
||||
element.getAttribute('data-participant-id'),
|
||||
element);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user