Échapper les noms de dalles injectés dans l'interface (HTML et <script> inline)
This commit is contained in:
@ -27,6 +27,7 @@ Intégration:
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@ -1189,11 +1190,23 @@ def _panel_selection(all_viz_keys):
|
||||
return ordered_viz, default_viz
|
||||
|
||||
|
||||
def _json_for_script(obj):
|
||||
"""JSON sûr à embarquer dans un <script> inline.
|
||||
|
||||
json.dumps n'échappe pas « </ » : un nom de dalle contenant
|
||||
« </script><script>… » sortirait du contexte JS (nom de dossier issu
|
||||
d'un fichier renommé dans input/). La séquence est neutralisée par un
|
||||
antislash devant la barre oblique, ce qui est identique pour le parseur
|
||||
JSON.
|
||||
"""
|
||||
return json.dumps(obj, ensure_ascii=False).replace("</", "<\\/")
|
||||
|
||||
|
||||
def _render_html(tiles, global_bbox, all_viz_keys, output_format):
|
||||
"""Construit la coquille HTML (données intégrées, styles/JS dans assets/)."""
|
||||
ordered_viz, default_viz = _panel_selection(all_viz_keys)
|
||||
|
||||
tiles_json = json.dumps(tiles, ensure_ascii=False)
|
||||
tiles_json = _json_for_script(tiles)
|
||||
|
||||
# Les assets sont référencés avec une version (hash de leur contenu) :
|
||||
# une URL neuve à chaque évolution du CSS/JS force le rechargement chez
|
||||
@ -1212,20 +1225,20 @@ def _render_html(tiles, global_bbox, all_viz_keys, output_format):
|
||||
gen_keys = [v for v in _VIZ_FALLBACK_ORDER if v in gen_base]
|
||||
gen_keys += sorted(k for k in gen_base if k not in gen_keys)
|
||||
gen_viz_options = "\n".join(
|
||||
f' <option value="{KEYWORD_TO_STEP.get(v, v)}" selected>'
|
||||
f'{VIZ_LABELS.get(v, v)}</option>'
|
||||
f' <option value="{html.escape(KEYWORD_TO_STEP.get(v, v))}" selected>'
|
||||
f'{html.escape(VIZ_LABELS.get(v, v))}</option>'
|
||||
for v in gen_keys)
|
||||
|
||||
viz_meta_json = json.dumps({
|
||||
viz_meta_json = _json_for_script({
|
||||
k: {'label': VIZ_LABELS.get(k, k)}
|
||||
for k in ordered_viz
|
||||
}, ensure_ascii=False)
|
||||
})
|
||||
|
||||
stats_json = json.dumps(
|
||||
stats_json = _json_for_script(
|
||||
{'default_viz': default_viz,
|
||||
'default_layers': default_layers_present(all_viz_keys),
|
||||
'n_tiles': len(tiles)},
|
||||
ensure_ascii=False)
|
||||
)
|
||||
|
||||
n_tiles = len(tiles)
|
||||
grid_w = global_bbox['max_col'] - global_bbox['min_col'] + 1
|
||||
@ -2320,7 +2333,7 @@ function renderLayerPanel() {
|
||||
'<span class="layer-grip" title="Glisser pour réordonner">⠿</span>' +
|
||||
'<button class="eye' + (state.layers.on[key] ? ' on' : '') + '" title="Afficher / masquer">' +
|
||||
(state.layers.on[key] ? SVG_EYE : SVG_EYE_OFF) + '</button>' +
|
||||
'<span class="layer-name">' + meta.label + '</span>' +
|
||||
'<span class="layer-name">' + escapeHtml(meta.label) + '</span>' +
|
||||
'<input type="range" min="0" max="100" step="1" value="' +
|
||||
Math.round(state.layers.opacity[key] * 100) + '" title="Opacité">' +
|
||||
'<span class="layer-pct">' + Math.round(state.layers.opacity[key] * 100) + '%</span>';
|
||||
@ -2832,6 +2845,13 @@ function methodLabel(m) {
|
||||
return { smrf: 'SMRF', csf: 'CSF', auto: 'Auto (détection)' }[m] || m;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
// Noms de dalles/couches insérés en innerHTML : jamais de HTML brut,
|
||||
// même si un fichier renommé dans input/ tente d'injecter du markup.
|
||||
return String(s).replace(/[&<>"']/g, c =>
|
||||
({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c]));
|
||||
}
|
||||
|
||||
function humanSize(bytes) {
|
||||
if (!Number.isFinite(bytes)) return '';
|
||||
if (bytes < 1024) return bytes + ' o';
|
||||
@ -2867,7 +2887,7 @@ function tileInfoContent(t) {
|
||||
'<span class="tile-info-badge">' + res + ' m/px</span>' +
|
||||
'<button class="tile-info-close" title="Fermer">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="tile-info-dalle" title="' + t.name + '">' + t.name + '</div>' +
|
||||
'<div class="tile-info-dalle" title="' + escapeHtml(t.name) + '">' + escapeHtml(t.name) + '</div>' +
|
||||
'<div class="tile-info-meta">' +
|
||||
(t.size_km || 1) + ' × ' + (t.size_km || 1) + ' km · ' + methodLabel(meta.method) +
|
||||
(meta.generated ? ' · généré le ' + fmtShortDate(meta.generated) : '') +
|
||||
@ -2891,10 +2911,10 @@ function tileInfoContent(t) {
|
||||
pill.className = 'vpill' + (state.layers.on[k] ? ' on' : '');
|
||||
pill.title = tip;
|
||||
pill.innerHTML =
|
||||
'<button class="vpill-eye" data-viz="' + k + '" title="Afficher / masquer cette couche">' +
|
||||
'<button class="vpill-eye" data-viz="' + escapeHtml(k) + '" title="Afficher / masquer cette couche">' +
|
||||
(state.layers.on[k] ? SVG_EYE : SVG_EYE_OFF) + '</button>' +
|
||||
'<button class="vpill-name" data-viz="' + k + '" title="Afficher / masquer cette couche">' + label + '</button>' +
|
||||
'<a class="vpill-dl" href="' + t.viz[k].full + '" download title="Télécharger l\'image">⤓</a>';
|
||||
'<button class="vpill-name" data-viz="' + escapeHtml(k) + '" title="Afficher / masquer cette couche">' + escapeHtml(label) + '</button>' +
|
||||
'<a class="vpill-dl" href="' + escapeHtml(t.viz[k].full) + '" download title="Télécharger l\'image">⤓</a>';
|
||||
pills.appendChild(pill);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user