From c1a2f38044705ee4ffa6619c8a93c6646b3402cd Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Mon, 20 Jul 2026 00:32:50 +0200 Subject: [PATCH] =?UTF-8?q?Convertit=20les=20endpoints=20de=20pr=C3=A9senc?= =?UTF-8?q?e=20en=20POST=20(anti=20CSRF=20trivial)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /presence/{token}/arrivee et /depart modifiaient l'état (save_presence, append_log) : c'est anti-pattern REST, CSRF-trivial via si le token fuite, et le token se retrouvait dans les logs reverse-proxy, l'historique navigateur et les referrers. Les deux routes passent en POST. C'est un breaking change pour les clients Tasker actuels, mais Tasker supporte HTTP POST nativement et la doc docs/notifications.md est mise à jour (Tasker : HTTP Request méthode POST ; script NetworkManager : curl -X POST). Les tests existants qui utilisaient client.get(...) sur ces routes passent en client.post(...). Un nouveau test vérifie que le GET renvoie désormais 405 Method Not Allowed et que le POST continue de fonctionner. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- app/main.py | 8 ++++---- app/tests/test_logs.py | 8 ++++---- app/tests/test_security.py | 23 +++++++++++++++++++++++ docs/notifications.md | 8 ++++---- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/main.py b/app/main.py index 84be412..41f103a 100644 --- a/app/main.py +++ b/app/main.py @@ -849,11 +849,11 @@ def settings_plages_save( return RedirectResponse("/settings?plages_saved=1", status_code=303) -@app.get("/presence/{token}/arrivee") +@app.post("/presence/{token}/arrivee") def presence_arrivee(token: str): user_id = find_user_by_token(token) if not user_id: - logger.warning("GET /presence/.../arrivee: token inconnu") + logger.warning("POST /presence/.../arrivee: token inconnu") return JSONResponse({"error": "invalid token"}, status_code=404) logger.info("présence: arrivée détectée pour %r", user_id) save_presence(user_id, present=True, since=datetime.now().isoformat()) @@ -861,11 +861,11 @@ def presence_arrivee(token: str): return {"ok": True} -@app.get("/presence/{token}/depart") +@app.post("/presence/{token}/depart") def presence_depart(token: str): user_id = find_user_by_token(token) if not user_id: - logger.warning("GET /presence/.../depart: token inconnu") + logger.warning("POST /presence/.../depart: token inconnu") return JSONResponse({"error": "invalid token"}, status_code=404) logger.info("présence: départ détecté pour %r", user_id) save_presence(user_id, present=False, since=datetime.now().isoformat()) diff --git a/app/tests/test_logs.py b/app/tests/test_logs.py index 0b5d64a..337e1c8 100644 --- a/app/tests/test_logs.py +++ b/app/tests/test_logs.py @@ -46,9 +46,9 @@ def test_logs_page_empty_for_new_user(): def test_logs_page_shows_entries_after_presence(): token = models.load_notif_config("alice")["token"] # Simule une arrivée Tasker - client.get(f"/presence/{token}/arrivee") + client.post(f"/presence/{token}/arrivee") # Simule un départ - client.get(f"/presence/{token}/depart") + client.post(f"/presence/{token}/depart") r = client.get("/logs", cookies=COOKIES) assert r.status_code == 200 @@ -267,12 +267,12 @@ def test_regenerate_presence_token_invalidates_old_urls(): assert "Token régénéré" in r.text # L'ancienne URL doit maintenant renvoyer 404 (token inconnu) - r_old = client.get(f"/presence/{old_token}/arrivee") + r_old = client.post(f"/presence/{old_token}/arrivee") assert r_old.status_code == 404 # La nouvelle URL doit fonctionner new_cfg = models.load_notif_config("alice") - r_new = client.get(f"/presence/{new_cfg['token']}/arrivee") + r_new = client.post(f"/presence/{new_cfg['token']}/arrivee") assert r_new.status_code == 200 assert r_new.json() == {"ok": True} diff --git a/app/tests/test_security.py b/app/tests/test_security.py index 73fc356..b856ef9 100644 --- a/app/tests/test_security.py +++ b/app/tests/test_security.py @@ -398,3 +398,26 @@ def test_csrf_accepts_get_regardless_of_origin(): r = client.get("/login", headers={"Origin": "https://evil.attacker"}) # 200 (page login) ou 302 (déjà loggué) mais pas 403 assert r.status_code != 403 + + +# ── Endpoints de présence : POST uniquement (anti CSRF trivial) ────────────── + +def test_presence_endpoints_require_post(): + """Les endpoints /presence/... doivent refuser les GET (CSRF trivial via ).""" + notif = models.load_notif_config("alice") if (models.DATA_DIR / "users" / "alice").exists() else None + # Crée alice si nécessaire et récupère un token valide + _seed_user("alice") + token = models.load_notif_config("alice")["token"] + + # GET doit échouer (405 Method Not Allowed) + r_get_arr = client.get(f"/presence/{token}/arrivee", follow_redirects=False) + r_get_dep = client.get(f"/presence/{token}/depart", follow_redirects=False) + assert r_get_arr.status_code == 405, r_get_arr.status_code + assert r_get_dep.status_code == 405 + + # POST doit fonctionner + r_post_arr = client.post(f"/presence/{token}/arrivee") + r_post_dep = client.post(f"/presence/{token}/depart") + assert r_post_arr.status_code == 200 + assert r_post_dep.status_code == 200 + assert r_post_arr.json() == {"ok": True} diff --git a/docs/notifications.md b/docs/notifications.md index ab085a2..f27a01b 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -32,10 +32,10 @@ contiennent un token secret, ne les partage pas. coupe la géolocalisation quand l'app n'est pas au premier plan). 3. Crée un profil : **Trigger → Location** → renseigne l'adresse/coordonnées du bureau et un rayon (~100–150 m) → événement **Enter**. - Tâche associée : **Net → HTTP Request**, méthode `GET`, URL = ton lien + Tâche associée : **Net → HTTP Request**, méthode `POST`, URL = ton lien **Arrivée** (copié depuis `/settings`). 4. Crée un second profil symétrique : même géofence, événement **Exit**, - tâche HTTP GET vers ton lien **Départ**. + tâche HTTP POST vers ton lien **Départ**. ### 3. Vérifier @@ -86,10 +86,10 @@ current_ssid=$(nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d: -f2) case "$2" in up) - [ "$current_ssid" = "$BUREAU_SSID" ] && curl -fsS "$URL_ARRIVEE" + [ "$current_ssid" = "$BUREAU_SSID" ] && curl -fsS -X POST "$URL_ARRIVEE" ;; down) - [ "$current_ssid" != "$BUREAU_SSID" ] && curl -fsS "$URL_DEPART" + [ "$current_ssid" != "$BUREAU_SSID" ] && curl -fsS -X POST "$URL_DEPART" ;; esac ```