fix(web): SRI sur les dépendances CDN et timeout des appels API

Les cinq bibliothèques chargées depuis unpkg et jsDelivr (Leaflet,
MarkerCluster, leaflet.heat, noUiSlider, flag-icons) n'avaient aucun contrôle
d'intégrité : un CDN compromis exécutait du code arbitraire sur le site public.
Les neuf hashes ont été recalculés depuis les URL versionnées.

GoatCounter reste sans SRI : son script n'est pas versionné.

apiGet() reçoit un timeout : sans lui, une API qui ne répond jamais laissait
l'interface et le rechargement du viewport bloqués indéfiniment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas FRYDER 2026-08-20 16:44:26 +02:00
parent da24d2a8a1
commit 1fe5b4a618
2 changed files with 31 additions and 14 deletions

View file

@ -173,11 +173,19 @@ const MACRO_GENRES = [
]; ];
// --- API --- // --- API ---
async function apiGet(path) { async function apiGet(path, timeoutMs = 15000) {
const url = `${API_BASE}${path}`; const url = `${API_BASE}${path}`;
const r = await fetch(url, { mode: "cors" }); // Timeout explicite : sans lui, une API qui ne répond jamais laisse l'UI (et
if (!r.ok) throw new Error(`API ${path} failed: ${r.status}`); // le rechargement du viewport de la carte) bloquée indéfiniment.
return await r.json(); const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const r = await fetch(url, { mode: "cors", signal: ctrl.signal });
if (!r.ok) throw new Error(`API ${path} failed: ${r.status}`);
return await r.json();
} finally {
clearTimeout(t);
}
} }
async function loadStats() { async function loadStats() {

View file

@ -11,26 +11,35 @@
<link href="https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&family=Inter:wght@300;400;600;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&family=Inter:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<!-- Leaflet --> <!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> integrity="sha384-sHL9NAb7lN7rfvG5lfHpm643Xkcjzp4jFvuavGOndn6pjVqS6ny56CAt3nsEVT4H" crossorigin="anonymous" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" crossorigin="anonymous"></script>
<!-- MarkerCluster --> <!-- MarkerCluster -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"/> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"/> integrity="sha384-pmjIAcz2bAn0xukfxADbZIb3t8oRT9Sv0rvO+BR5Csr6Dhqq+nZs59P0pPKQJkEV" crossorigin="anonymous"/>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
integrity="sha384-wgw+aLYNQ7dlhK47ZPK7FRACiq7ROZwgFNg0m04avm4CaXS+Z9Y7nMu8yNjBKYC+" crossorigin="anonymous"/>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"
integrity="sha384-eXVCORTRlv4FUUgS/xmOyr66XBVraen8ATNLMESp92FKXLAMiKkerixTiBvXriZr" crossorigin="anonymous"></script>
<!-- Heatmap --> <!-- Heatmap -->
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script> <script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"
integrity="sha384-mFKkGiGvT5vo1fEyGCD3hshDdKmW3wzXW/x+fWriYJArD0R3gawT6lMvLboM22c0" crossorigin="anonymous"></script>
<!-- noUiSlider (double slider timeline) --> <!-- noUiSlider (double slider timeline) -->
<link rel="stylesheet" href="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.css"> <link rel="stylesheet" href="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.css"
<script src="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.js"></script> integrity="sha384-PSZaVsyG9jDu8hFaSJev5s/9poIJlX7cuxSGdqCgXRHpo2DzIaZAyCd2rG/DJJmV" crossorigin="anonymous">
<script src="https://unpkg.com/nouislider@15.7.1/dist/nouislider.min.js"
integrity="sha384-/gBUOLHADjY2rp6bHB0IyW9AC28q4OsnirJScje4l1crgYW7Qarx3dH8zcqcUgmy" crossorigin="anonymous"></script>
<!-- GoatCounter --> <!-- GoatCounter (script d'analytics non versionné → pas de SRI possible) -->
<script data-goatcounter="https://metalfromeurope.goatcounter.com/count" <script data-goatcounter="https://metalfromeurope.goatcounter.com/count"
async src="//gc.zgo.at/count.js"></script> async src="//gc.zgo.at/count.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"
integrity="sha384-aQuvIWWIbpu/mSqULLDiUveyYiPoJzPKAWjUmGJ+Elm+N/LJhzfZqsutsfw870JS" crossorigin="anonymous">
<link rel="stylesheet" href="./styles.css" /> <link rel="stylesheet" href="./styles.css" />
</head> </head>