fix(kiosk): escape data-derived strings in innerHTML (RG-T15) (#20)
Some checks failed
CI / secret-scan (push) Has been cancelled
CI / php-lint (push) Has been cancelled
CI / static-tests (push) Has been cancelled
CI / auto-merge (push) Has been cancelled

This commit is contained in:
Corentin JOGUET 2026-06-16 14:20:50 +02:00
parent ee14186a19
commit 9ddb4ccb27
4 changed files with 41 additions and 21 deletions

View file

@ -17,7 +17,7 @@
* requires prices shown to end-consumers to include all taxes.
*/
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice } from './state.js';
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice, escHtml } from './state.js';
import { refreshCartBadge } from './nav.js';
/* TVA rate used for display breakdown only — stored prices are already TTC */
@ -62,27 +62,27 @@ function renderCart() {
row.innerHTML = `
<img
class="cart-line__image"
src="${item.image}"
alt="${item.libelle}"
src="${escHtml(item.image)}"
alt="${escHtml(item.libelle)}"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
<div class="cart-line__info">
<span class="cart-line__name">${item.libelle}</span>
<span class="cart-line__name">${escHtml(item.libelle)}</span>
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite${isMenu && (item.supplement_cents ?? 0) > 0 ? ` + ${formatPrice(item.supplement_cents)} suppl.` : ''}</span>
${isMenu && item.composition ? renderCompositionBlock(item) : ''}
</div>
<div class="cart-line__qty" role="group" aria-label="Quantite de ${item.libelle}">
<div class="cart-line__qty" role="group" aria-label="Quantite de ${escHtml(item.libelle)}">
<button
class="qty-btn qty-btn--minus"
data-index="${index}"
aria-label="Diminuer la quantite de ${item.libelle}"
aria-label="Diminuer la quantite de ${escHtml(item.libelle)}"
type="button"
>-</button>
<span class="qty-value" aria-live="polite">${item.quantite}</span>
<button
class="qty-btn qty-btn--plus"
data-index="${index}"
aria-label="Augmenter la quantite de ${item.libelle}"
aria-label="Augmenter la quantite de ${escHtml(item.libelle)}"
type="button"
>+</button>
</div>
@ -90,7 +90,7 @@ function renderCart() {
<button
class="cart-line__remove"
data-index="${index}"
aria-label="Supprimer ${item.libelle} du panier"
aria-label="Supprimer ${escHtml(item.libelle)} du panier"
type="button"
>
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
@ -161,10 +161,10 @@ function renderCompositionBlock(item) {
return `
<ul class="cart-line__composition" aria-label="Composition du menu">
<li class="cart-line__comp-item">+ ${c.burger.libelle}${burgerOpts}</li>
<li class="cart-line__comp-item">+ ${c.accompagnement.libelle}${accompTailleLabel}</li>
<li class="cart-line__comp-item">+ ${c.boisson.libelle}${boissonTailleLabel}</li>
<li class="cart-line__comp-item">+ ${c.sauce.libelle}</li>
<li class="cart-line__comp-item">+ ${escHtml(c.burger.libelle)}${burgerOpts}</li>
<li class="cart-line__comp-item">+ ${escHtml(c.accompagnement.libelle)}${accompTailleLabel}</li>
<li class="cart-line__comp-item">+ ${escHtml(c.boisson.libelle)}${boissonTailleLabel}</li>
<li class="cart-line__comp-item">+ ${escHtml(c.sauce.libelle)}</li>
${supplTotal > 0 ? `<li class="cart-line__comp-suppl">Supplement ${nbGrandes} grande(s) : +${formatPrice(supplTotal)}</li>` : ''}
</ul>
`;

View file

@ -16,7 +16,7 @@
*/
import { findProduct } from './data.js';
import { addToCart, formatPrice } from './state.js';
import { addToCart, formatPrice, escHtml } from './state.js';
import { refreshCartBadge } from './nav.js';
import { openMenuComposer } from './page-product-menu.js';
@ -59,18 +59,18 @@ async function renderProduct() {
<div class="product-detail__image-wrap">
<img
class="product-detail__image"
src="${product.image}"
alt="${product.nom}"
src="${escHtml(product.image)}"
alt="${escHtml(product.nom)}"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-detail__info">
<h1 class="product-detail__name">${product.nom}</h1>
<h1 class="product-detail__name">${escHtml(product.nom)}</h1>
<p class="product-detail__price">${formatPrice(product.prix)}</p>
<button
class="btn btn--primary btn--large product-detail__add"
id="add-to-cart-btn"
aria-label="Ajouter ${product.nom} au panier"
aria-label="Ajouter ${escHtml(product.nom)} au panier"
type="button"
>
Ajouter au panier

View file

@ -7,7 +7,7 @@
*/
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
import { formatPrice } from './state.js';
import { formatPrice, escHtml } from './state.js';
const params = new URLSearchParams(window.location.search);
const categoryId = parseInt(params.get('category'), 10) || 1;
@ -60,14 +60,14 @@ async function renderProducts() {
<div class="product-card__image-wrap">
<img
class="product-card__image"
src="${product.image}"
alt="${product.nom}"
src="${escHtml(product.image)}"
alt="${escHtml(product.nom)}"
loading="lazy"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-card__body">
<span class="product-card__name">${product.nom}</span>
<span class="product-card__name">${escHtml(product.nom)}</span>
<span class="product-card__price">${formatPrice(product.prix)}</span>
</div>
`;

View file

@ -172,3 +172,23 @@ export function formatPrice(cents) {
export function getCartCount() {
return getCart().reduce((sum, item) => sum + item.quantite, 0);
}
/* --- HTML escaping ------------------------------------------------------- */
/**
* Minimal HTML escaping for data-derived strings (product names, image paths,
* libelles) injected into innerHTML. RG-T15 (anti-XSS) requires every catalogue
* value rendered as HTML to be escaped. Centralised here so all page modules
* that build markup from data escape identically; mirrors the helper that was
* local to page-product-menu.js.
* @param {*} str
* @returns {string}
*/
export function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}