From a17c362c81d04bcfa7c949dba74dd1dc0b4a79ee Mon Sep 17 00:00:00 2001 From: Rene Luria Date: Mon, 29 Jun 2026 20:05:45 +0200 Subject: [PATCH] fix(frontend): Sanitize DOM rendering to prevent XSS via innerHTML Replace unsafe innerHTML template literals with document.createElement() and textContent for all user-conrolled data fields in index.html. Event cards and modal details previously interpolated API responses (event.name, event.title, player.fname, staff.role, etc.) directly into innerHTML without escaping, allowing stored XSS if upstream MyIce data is malicious (CWE-79 / vuln-0002). All dynamic text now flows through textContent or createTextNode(), while static HTML structures (SVG icons, strong labels) remain safe hardcoded innerHTML assignments. Refs vuln-0002 --- index.html | 346 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 199 insertions(+), 147 deletions(-) diff --git a/index.html b/index.html index 5c81b76..1fad84d 100644 --- a/index.html +++ b/index.html @@ -545,40 +545,49 @@ filteredEvents.forEach(event => { const eventCard = document.createElement("div"); eventCard.className = "bg-white rounded-2xl shadow-lg overflow-hidden transform hover:scale-105 transition-all duration-300 cursor-pointer"; - eventCard.innerHTML = ` -
-
-
-

${event.agegroup} - ${event.name}

-

${event.title}

-
-
-
-

- - - - Adversaire: ${event.opponent} -

+ const card = document.createElement("div"); + card.className = "p-6 border-l-4"; + card.style.borderColor = event.color || "#818cf8"; -

- - - - - Lieu: ${event.place} -

+ const inner = document.createElement("div"); + inner.className = "flex justify-between items-start"; -

- - - - Heure: ${event.start} - ${event.end} -

-
-
- `; + const innerDiv = document.createElement("div"); + const h3 = document.createElement("h3"); + h3.className = "text-xl font-bold text-gray-800"; + h3.textContent = event.agegroup + " - " + event.name; + const subtitle = document.createElement("p"); + subtitle.className = "text-gray-600 mt-1"; + subtitle.textContent = event.title; + innerDiv.appendChild(h3); + innerDiv.appendChild(subtitle); + inner.appendChild(innerDiv); + card.appendChild(inner); + + const detailsDiv = document.createElement("div"); + detailsDiv.className = "mt-4 space-y-2"; + + const p1 = document.createElement("p"); + p1.className = "flex items-center text-gray-700"; + p1.innerHTML = 'Adversaire: '; + p1.appendChild(document.createTextNode(event.opponent)); + detailsDiv.appendChild(p1); + + const p2 = document.createElement("p"); + p2.className = "flex items-center text-gray-700"; + p2.innerHTML = 'Lieu: '; + p2.appendChild(document.createTextNode(event.place)); + detailsDiv.appendChild(p2); + + const p3 = document.createElement("p"); + p3.className = "flex items-center text-gray-700"; + p3.innerHTML = 'Heure: '; + p3.appendChild(document.createTextNode(event.start + " - " + event.end)); + detailsDiv.appendChild(p3); + + card.appendChild(detailsDiv); + eventCard.appendChild(card); eventCard.addEventListener("click", () => fetchEventDetails(event.id_event)); eventList.appendChild(eventCard); }); @@ -591,129 +600,172 @@ }) .then(response => response.json()) .then(data => { - // Check if available players data exists const availablePlayers = data.convocation.available || []; const sortedPlayers = availablePlayers .sort((a, b) => (a.number || 0) - (b.number || 0)); - // Calculate player statistics - const totalPlayers = sortedPlayers.length; - const positionCount = {}; - sortedPlayers.forEach(player => { - const position = player.position || "N/A"; - positionCount[position] = (positionCount[position] || 0) + 1; - }); + const totalPlayers = sortedPlayers.length; + const positionCount = {}; + sortedPlayers.forEach(player => { + const position = player.position || "N/A"; + positionCount[position] = (positionCount[position] || 0) + 1; + }); - // Generate position breakdown - const positionBreakdown = Object.entries(positionCount) - .map(([position, count]) => `${position}: ${count}`) - .join(', '); + const positionBreakdown = Object.entries(positionCount) + .map(([position, count]) => position + ": " + count) + .join(", "); - // Sort players by position first, then by number - const playersByPosition = [...sortedPlayers].sort((a, b) => { - // Sort by position first - const positionA = a.position || "ZZZ"; // Put undefined positions at the end - const positionB = b.position || "ZZZ"; - - if (positionA !== positionB) { - return positionA.localeCompare(positionB); - } - - // If positions are the same, sort by number - const numA = parseInt(a.number) || 0; - const numB = parseInt(b.number) || 0; - return numA - numB; - }); - - // Process staff data - const staffList = data.convocation.staff || []; - const totalStaff = staffList.length; - - // Check if there are no players - if (totalPlayers === 0 && totalStaff === 0) { - eventDetailsContent.innerHTML = ` -
-
-
${data.title}
-
-

Type: ${data.type}

-

Lieu: ${data.place}

-

Heure: ${data.time_start} - ${data.time_end}

-
-
-
Aucun joueur ni personnel convoqué
-

Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.

-
-
-
- `; - } else { - let staffHtml = ''; - if (totalStaff > 0) { - staffHtml = ` -
-
Personnel (${totalStaff}):
- -
- `; - } else { - staffHtml = ` -
-
Aucun personnel convoqué
-

Il n'y a actuellement aucun personnel convoqué pour ce match.

-
- `; - } - - if (totalPlayers === 0) { - eventDetailsContent.innerHTML = ` -
-
${data.title}
-
-

Type: ${data.type}

-

Lieu: ${data.place}

-

Heure: ${data.time_start} - ${data.time_end}

-
-
-
Aucun joueur convoqué
-

Il n'y a actuellement aucun joueur convoqué pour ce match.

-
- ${staffHtml} -
- `; - } else { - eventDetailsContent.innerHTML = ` -
-
${data.title}
-
-

Type: ${data.type}

-

Lieu: ${data.place}

-

Heure: ${data.time_start} - ${data.time_end}

-

Joueurs convoqués: ${totalPlayers} joueur${totalPlayers > 1 ? 's' : ''} (${positionBreakdown})

-
- -
-
Liste des joueurs:
-
    - ${playersByPosition.map(player => { - let number = player.number ? player.number : "N/A"; - let position = player.position ? player.position : "N/A"; - return `
  • [${position}] ${number} - ${player.fname} ${player.lname} (${player.dob})
  • `; - }).join('')} -
-
- - ${staffHtml} -
- `; - } + const playersByPosition = [...sortedPlayers].sort((a, b) => { + const positionA = a.position || "ZZZ"; + const positionB = b.position || "ZZZ"; + if (positionA !== positionB) { + return positionA.localeCompare(positionB); } - eventDetailsModal.classList.remove("hidden"); - }) + const numA = parseInt(a.number) || 0; + const numB = parseInt(b.number) || 0; + return numA - numB; + }); + + const staffList = data.convocation.staff || []; + const totalStaff = staffList.length; + + eventDetailsContent.innerHTML = ""; + + if (totalPlayers === 0 && totalStaff === 0) { + const wrapper = document.createElement("div"); + wrapper.className = "rounded-xl border border-amber-300 bg-amber-50 p-6"; + const centerDiv = document.createElement("div"); + centerDiv.className = "text-center"; + const h5 = document.createElement("h5"); + h5.className = "text-2xl font-bold text-gray-800 mb-4"; + h5.textContent = data.title; + centerDiv.appendChild(h5); + + const infoDiv = document.createElement("div"); + infoDiv.className = "space-y-2 text-gray-700 mb-6"; + const pType = document.createElement("p"); + pType.innerHTML = "Type: "; + pType.appendChild(document.createTextNode(data.type)); + infoDiv.appendChild(pType); + const pLieu = document.createElement("p"); + pLieu.innerHTML = "Lieu: "; + pLieu.appendChild(document.createTextNode(data.place)); + infoDiv.appendChild(pLieu); + const pHeure = document.createElement("p"); + pHeure.innerHTML = "Heure: "; + pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end)); + infoDiv.appendChild(pHeure); + centerDiv.appendChild(infoDiv); + + const msgDiv = document.createElement("div"); + msgDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg"; + const msgH6 = document.createElement("h6"); + msgH6.className = "font-bold text-lg mb-1"; + msgH6.textContent = "Aucun joueur ni personnel convoqué"; + msgDiv.appendChild(msgH6); + const msgP = document.createElement("p"); + msgP.textContent = "Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match."; + msgDiv.appendChild(msgP); + centerDiv.appendChild(msgDiv); + wrapper.appendChild(centerDiv); + eventDetailsContent.appendChild(wrapper); + } else { + const container = document.createElement("div"); + const h5 = document.createElement("h5"); + h5.className = "text-2xl font-bold text-gray-800 mb-4"; + h5.textContent = data.title; + container.appendChild(h5); + + const infoDiv = document.createElement("div"); + infoDiv.className = "space-y-2 text-gray-700 mb-6"; + const pType = document.createElement("p"); + pType.innerHTML = "Type: "; + pType.appendChild(document.createTextNode(data.type)); + infoDiv.appendChild(pType); + const pLieu = document.createElement("p"); + pLieu.innerHTML = "Lieu: "; + pLieu.appendChild(document.createTextNode(data.place)); + infoDiv.appendChild(pLieu); + const pHeure = document.createElement("p"); + pHeure.innerHTML = "Heure: "; + pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end)); + infoDiv.appendChild(pHeure); + container.appendChild(infoDiv); + + if (totalPlayers === 0) { + const noPlayersDiv = document.createElement("div"); + noPlayersDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg"; + const noPlayersH6 = document.createElement("h6"); + noPlayersH6.className = "font-bold mb-1"; + noPlayersH6.textContent = "Aucun joueur convoqué"; + noPlayersDiv.appendChild(noPlayersH6); + const noPlayersP = document.createElement("p"); + noPlayersP.textContent = "Il n'y a actuellement aucun joueur convoqué pour ce match."; + noPlayersDiv.appendChild(noPlayersP); + container.appendChild(noPlayersDiv); + } else { + const playersP = document.createElement("p"); + playersP.innerHTML = "Joueurs convoqués: "; + playersP.appendChild(document.createTextNode(totalPlayers + " joueur" + (totalPlayers > 1 ? "s" : "") + " (" + positionBreakdown + ")")); + infoDiv.appendChild(playersP); + + const playersDiv = document.createElement("div"); + const playersH6 = document.createElement("h6"); + playersH6.className = "text-xl font-semibold text-gray-800 mb-3"; + playersH6.textContent = "Liste des joueurs:"; + playersDiv.appendChild(playersH6); + const playersUl = document.createElement("ul"); + playersUl.className = "grid grid-cols-1 md:grid-cols-2 gap-3"; + playersByPosition.forEach(player => { + const li = document.createElement("li"); + li.className = "bg-indigo-50 rounded-lg p-3"; + const number = player.number ? player.number : "N/A"; + const position = player.position ? player.position : "N/A"; + li.textContent = "[" + position + "] " + number + " - " + player.fname + " " + player.lname + " (" + player.dob + ")"; + playersUl.appendChild(li); + }); + playersDiv.appendChild(playersUl); + container.appendChild(playersDiv); + } + + if (totalStaff > 0) { + const staffDiv = document.createElement("div"); + staffDiv.className = "mt-6"; + const staffH6 = document.createElement("h6"); + staffH6.className = "text-xl font-semibold text-gray-800 mb-3"; + staffH6.textContent = "Personnel (" + totalStaff + "):"; + staffDiv.appendChild(staffH6); + const staffUl = document.createElement("ul"); + staffUl.className = "space-y-2"; + staffList.forEach(staff => { + const li = document.createElement("li"); + li.className = "flex items-center"; + const roleSpan = document.createElement("span"); + roleSpan.className = "font-medium w-32"; + roleSpan.textContent = staff.role + ":"; + li.appendChild(roleSpan); + li.appendChild(document.createTextNode(" " + staff.fname + " " + staff.lname)); + staffUl.appendChild(li); + }); + staffDiv.appendChild(staffUl); + container.appendChild(staffDiv); + } else { + const noStaffDiv = document.createElement("div"); + noStaffDiv.className = "mt-6 bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg"; + const noStaffH6 = document.createElement("h6"); + noStaffH6.className = "font-bold mb-1"; + noStaffH6.textContent = "Aucun personnel convoqué"; + noStaffDiv.appendChild(noStaffH6); + const noStaffP = document.createElement("p"); + noStaffP.textContent = "Il n'y a actuellement aucun personnel convoqué pour ce match."; + noStaffDiv.appendChild(noStaffP); + container.appendChild(noStaffDiv); + } + + eventDetailsContent.appendChild(container); + } + eventDetailsModal.classList.remove("hidden"); + }) .catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error)); } });