É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 hashlib
|
||||||
|
import html
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -1189,11 +1190,23 @@ def _panel_selection(all_viz_keys):
|
|||||||
return ordered_viz, default_viz
|
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):
|
def _render_html(tiles, global_bbox, all_viz_keys, output_format):
|
||||||
"""Construit la coquille HTML (données intégrées, styles/JS dans assets/)."""
|
"""Construit la coquille HTML (données intégrées, styles/JS dans assets/)."""
|
||||||
ordered_viz, default_viz = _panel_selection(all_viz_keys)
|
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) :
|
# 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
|
# 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 = [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_keys += sorted(k for k in gen_base if k not in gen_keys)
|
||||||
gen_viz_options = "\n".join(
|
gen_viz_options = "\n".join(
|
||||||
f' <option value="{KEYWORD_TO_STEP.get(v, v)}" selected>'
|
f' <option value="{html.escape(KEYWORD_TO_STEP.get(v, v))}" selected>'
|
||||||
f'{VIZ_LABELS.get(v, v)}</option>'
|
f'{html.escape(VIZ_LABELS.get(v, v))}</option>'
|
||||||
for v in gen_keys)
|
for v in gen_keys)
|
||||||
|
|
||||||
viz_meta_json = json.dumps({
|
viz_meta_json = _json_for_script({
|
||||||
k: {'label': VIZ_LABELS.get(k, k)}
|
k: {'label': VIZ_LABELS.get(k, k)}
|
||||||
for k in ordered_viz
|
for k in ordered_viz
|
||||||
}, ensure_ascii=False)
|
})
|
||||||
|
|
||||||
stats_json = json.dumps(
|
stats_json = _json_for_script(
|
||||||
{'default_viz': default_viz,
|
{'default_viz': default_viz,
|
||||||
'default_layers': default_layers_present(all_viz_keys),
|
'default_layers': default_layers_present(all_viz_keys),
|
||||||
'n_tiles': len(tiles)},
|
'n_tiles': len(tiles)},
|
||||||
ensure_ascii=False)
|
)
|
||||||
|
|
||||||
n_tiles = len(tiles)
|
n_tiles = len(tiles)
|
||||||
grid_w = global_bbox['max_col'] - global_bbox['min_col'] + 1
|
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>' +
|
'<span class="layer-grip" title="Glisser pour réordonner">⠿</span>' +
|
||||||
'<button class="eye' + (state.layers.on[key] ? ' on' : '') + '" title="Afficher / masquer">' +
|
'<button class="eye' + (state.layers.on[key] ? ' on' : '') + '" title="Afficher / masquer">' +
|
||||||
(state.layers.on[key] ? SVG_EYE : SVG_EYE_OFF) + '</button>' +
|
(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="' +
|
'<input type="range" min="0" max="100" step="1" value="' +
|
||||||
Math.round(state.layers.opacity[key] * 100) + '" title="Opacité">' +
|
Math.round(state.layers.opacity[key] * 100) + '" title="Opacité">' +
|
||||||
'<span class="layer-pct">' + Math.round(state.layers.opacity[key] * 100) + '%</span>';
|
'<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;
|
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) {
|
function humanSize(bytes) {
|
||||||
if (!Number.isFinite(bytes)) return '';
|
if (!Number.isFinite(bytes)) return '';
|
||||||
if (bytes < 1024) return bytes + ' o';
|
if (bytes < 1024) return bytes + ' o';
|
||||||
@ -2867,7 +2887,7 @@ function tileInfoContent(t) {
|
|||||||
'<span class="tile-info-badge">' + res + ' m/px</span>' +
|
'<span class="tile-info-badge">' + res + ' m/px</span>' +
|
||||||
'<button class="tile-info-close" title="Fermer">×</button>' +
|
'<button class="tile-info-close" title="Fermer">×</button>' +
|
||||||
'</div>' +
|
'</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">' +
|
'<div class="tile-info-meta">' +
|
||||||
(t.size_km || 1) + ' × ' + (t.size_km || 1) + ' km · ' + methodLabel(meta.method) +
|
(t.size_km || 1) + ' × ' + (t.size_km || 1) + ' km · ' + methodLabel(meta.method) +
|
||||||
(meta.generated ? ' · généré le ' + fmtShortDate(meta.generated) : '') +
|
(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.className = 'vpill' + (state.layers.on[k] ? ' on' : '');
|
||||||
pill.title = tip;
|
pill.title = tip;
|
||||||
pill.innerHTML =
|
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>' +
|
(state.layers.on[k] ? SVG_EYE : SVG_EYE_OFF) + '</button>' +
|
||||||
'<button class="vpill-name" data-viz="' + k + '" title="Afficher / masquer cette couche">' + label + '</button>' +
|
'<button class="vpill-name" data-viz="' + escapeHtml(k) + '" title="Afficher / masquer cette couche">' + escapeHtml(label) + '</button>' +
|
||||||
'<a class="vpill-dl" href="' + t.viz[k].full + '" download title="Télécharger l\'image">⤓</a>';
|
'<a class="vpill-dl" href="' + escapeHtml(t.viz[k].full) + '" download title="Télécharger l\'image">⤓</a>';
|
||||||
pills.appendChild(pill);
|
pills.appendChild(pill);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1045,7 +1045,8 @@ def _merge_remote_index():
|
|||||||
try:
|
try:
|
||||||
html = html_path.read_text(encoding="utf-8")
|
html = html_path.read_text(encoding="utf-8")
|
||||||
new_html = re.sub(r"const TILES = \[.*?\];",
|
new_html = re.sub(r"const TILES = \[.*?\];",
|
||||||
"const TILES = " + json.dumps(merged, ensure_ascii=False) + ";",
|
"const TILES = " + json.dumps(merged, ensure_ascii=False)
|
||||||
|
.replace("</", "<\\/") + ";",
|
||||||
html, count=1, flags=re.S)
|
html, count=1, flags=re.S)
|
||||||
if new_html != html:
|
if new_html != html:
|
||||||
html_path.write_text(new_html, encoding="utf-8")
|
html_path.write_text(new_html, encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user