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
This commit is contained in:
2026-06-29 20:05:45 +02:00
parent 77b2531106
commit a17c362c81
+170 -118
View File
@@ -545,40 +545,49 @@
filteredEvents.forEach(event => { filteredEvents.forEach(event => {
const eventCard = document.createElement("div"); 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.className = "bg-white rounded-2xl shadow-lg overflow-hidden transform hover:scale-105 transition-all duration-300 cursor-pointer";
eventCard.innerHTML = `
<div class="p-6 border-l-4" style="border-color: ${event.color || '#818cf8'}">
<div class="flex justify-between items-start">
<div>
<h3 class="text-xl font-bold text-gray-800">${event.agegroup} - ${event.name}</h3>
<p class="text-gray-600 mt-1">${event.title}</p>
</div>
</div>
<div class="mt-4 space-y-2"> const card = document.createElement("div");
<p class="flex items-center text-gray-700"> card.className = "p-6 border-l-4";
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> card.style.borderColor = event.color || "#818cf8";
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<strong>Adversaire:</strong> ${event.opponent}
</p>
<p class="flex items-center text-gray-700"> const inner = document.createElement("div");
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> inner.className = "flex justify-between items-start";
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<strong>Lieu:</strong> ${event.place}
</p>
<p class="flex items-center text-gray-700"> const innerDiv = document.createElement("div");
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> const h3 = document.createElement("h3");
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> h3.className = "text-xl font-bold text-gray-800";
</svg> h3.textContent = event.agegroup + " - " + event.name;
<strong>Heure:</strong> ${event.start} - ${event.end} const subtitle = document.createElement("p");
</p> subtitle.className = "text-gray-600 mt-1";
</div> subtitle.textContent = event.title;
</div> 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 = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg><strong>Adversaire:</strong> ';
p1.appendChild(document.createTextNode(event.opponent));
detailsDiv.appendChild(p1);
const p2 = document.createElement("p");
p2.className = "flex items-center text-gray-700";
p2.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg><strong>Lieu:</strong> ';
p2.appendChild(document.createTextNode(event.place));
detailsDiv.appendChild(p2);
const p3 = document.createElement("p");
p3.className = "flex items-center text-gray-700";
p3.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><strong>Heure:</strong> ';
p3.appendChild(document.createTextNode(event.start + " - " + event.end));
detailsDiv.appendChild(p3);
card.appendChild(detailsDiv);
eventCard.appendChild(card);
eventCard.addEventListener("click", () => fetchEventDetails(event.id_event)); eventCard.addEventListener("click", () => fetchEventDetails(event.id_event));
eventList.appendChild(eventCard); eventList.appendChild(eventCard);
}); });
@@ -591,12 +600,10 @@
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Check if available players data exists
const availablePlayers = data.convocation.available || []; const availablePlayers = data.convocation.available || [];
const sortedPlayers = availablePlayers const sortedPlayers = availablePlayers
.sort((a, b) => (a.number || 0) - (b.number || 0)); .sort((a, b) => (a.number || 0) - (b.number || 0));
// Calculate player statistics
const totalPlayers = sortedPlayers.length; const totalPlayers = sortedPlayers.length;
const positionCount = {}; const positionCount = {};
sortedPlayers.forEach(player => { sortedPlayers.forEach(player => {
@@ -604,113 +611,158 @@
positionCount[position] = (positionCount[position] || 0) + 1; positionCount[position] = (positionCount[position] || 0) + 1;
}); });
// Generate position breakdown
const positionBreakdown = Object.entries(positionCount) const positionBreakdown = Object.entries(positionCount)
.map(([position, count]) => `${position}: ${count}`) .map(([position, count]) => position + ": " + count)
.join(', '); .join(", ");
// Sort players by position first, then by number
const playersByPosition = [...sortedPlayers].sort((a, b) => { const playersByPosition = [...sortedPlayers].sort((a, b) => {
// Sort by position first const positionA = a.position || "ZZZ";
const positionA = a.position || "ZZZ"; // Put undefined positions at the end
const positionB = b.position || "ZZZ"; const positionB = b.position || "ZZZ";
if (positionA !== positionB) { if (positionA !== positionB) {
return positionA.localeCompare(positionB); return positionA.localeCompare(positionB);
} }
// If positions are the same, sort by number
const numA = parseInt(a.number) || 0; const numA = parseInt(a.number) || 0;
const numB = parseInt(b.number) || 0; const numB = parseInt(b.number) || 0;
return numA - numB; return numA - numB;
}); });
// Process staff data
const staffList = data.convocation.staff || []; const staffList = data.convocation.staff || [];
const totalStaff = staffList.length; const totalStaff = staffList.length;
// Check if there are no players eventDetailsContent.innerHTML = "";
if (totalPlayers === 0 && totalStaff === 0) { if (totalPlayers === 0 && totalStaff === 0) {
eventDetailsContent.innerHTML = ` const wrapper = document.createElement("div");
<div class="rounded-xl border border-amber-300 bg-amber-50 p-6"> wrapper.className = "rounded-xl border border-amber-300 bg-amber-50 p-6";
<div class="text-center"> const centerDiv = document.createElement("div");
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5> centerDiv.className = "text-center";
<div class="space-y-2 text-gray-700 mb-6"> const h5 = document.createElement("h5");
<p><strong>Type:</strong> ${data.type}</p> h5.className = "text-2xl font-bold text-gray-800 mb-4";
<p><strong>Lieu:</strong> ${data.place}</p> h5.textContent = data.title;
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p> centerDiv.appendChild(h5);
</div>
<div class="bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg"> const infoDiv = document.createElement("div");
<h6 class="font-bold text-lg mb-1">Aucun joueur ni personnel convoqué</h6> infoDiv.className = "space-y-2 text-gray-700 mb-6";
<p>Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.</p> const pType = document.createElement("p");
</div> pType.innerHTML = "<strong>Type:</strong> ";
</div> pType.appendChild(document.createTextNode(data.type));
</div> infoDiv.appendChild(pType);
`; const pLieu = document.createElement("p");
pLieu.innerHTML = "<strong>Lieu:</strong> ";
pLieu.appendChild(document.createTextNode(data.place));
infoDiv.appendChild(pLieu);
const pHeure = document.createElement("p");
pHeure.innerHTML = "<strong>Heure:</strong> ";
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 { } else {
let staffHtml = ''; const container = document.createElement("div");
if (totalStaff > 0) { const h5 = document.createElement("h5");
staffHtml = ` h5.className = "text-2xl font-bold text-gray-800 mb-4";
<div class="mt-6"> h5.textContent = data.title;
<h6 class="text-xl font-semibold text-gray-800 mb-3">Personnel (${totalStaff}):</h6> container.appendChild(h5);
<ul class="space-y-2">
${staffList.map(staff => { const infoDiv = document.createElement("div");
return `<li class="flex items-center"><span class="font-medium w-32">${staff.role}:</span> <span>${staff.fname} ${staff.lname}</span></li>`; infoDiv.className = "space-y-2 text-gray-700 mb-6";
}).join('')} const pType = document.createElement("p");
</ul> pType.innerHTML = "<strong>Type:</strong> ";
</div> pType.appendChild(document.createTextNode(data.type));
`; infoDiv.appendChild(pType);
} else { const pLieu = document.createElement("p");
staffHtml = ` pLieu.innerHTML = "<strong>Lieu:</strong> ";
<div class="mt-6 bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg"> pLieu.appendChild(document.createTextNode(data.place));
<h6 class="font-bold mb-1">Aucun personnel convoqué</h6> infoDiv.appendChild(pLieu);
<p>Il n'y a actuellement aucun personnel convoqué pour ce match.</p> const pHeure = document.createElement("p");
</div> pHeure.innerHTML = "<strong>Heure:</strong> ";
`; pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end));
} infoDiv.appendChild(pHeure);
container.appendChild(infoDiv);
if (totalPlayers === 0) { if (totalPlayers === 0) {
eventDetailsContent.innerHTML = ` const noPlayersDiv = document.createElement("div");
<div> noPlayersDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg";
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5> const noPlayersH6 = document.createElement("h6");
<div class="space-y-2 text-gray-700 mb-6"> noPlayersH6.className = "font-bold mb-1";
<p><strong>Type:</strong> ${data.type}</p> noPlayersH6.textContent = "Aucun joueur convoqué";
<p><strong>Lieu:</strong> ${data.place}</p> noPlayersDiv.appendChild(noPlayersH6);
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p> const noPlayersP = document.createElement("p");
</div> noPlayersP.textContent = "Il n'y a actuellement aucun joueur convoqué pour ce match.";
<div class="bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg"> noPlayersDiv.appendChild(noPlayersP);
<h6 class="font-bold mb-1">Aucun joueur convoqué</h6> container.appendChild(noPlayersDiv);
<p>Il n'y a actuellement aucun joueur convoqué pour ce match.</p>
</div>
${staffHtml}
</div>
`;
} else { } else {
eventDetailsContent.innerHTML = ` const playersP = document.createElement("p");
<div> playersP.innerHTML = "<strong>Joueurs convoqués:</strong> ";
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5> playersP.appendChild(document.createTextNode(totalPlayers + " joueur" + (totalPlayers > 1 ? "s" : "") + " (" + positionBreakdown + ")"));
<div class="space-y-2 text-gray-700 mb-6"> infoDiv.appendChild(playersP);
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<p><strong>Joueurs convoqués:</strong> ${totalPlayers} joueur${totalPlayers > 1 ? 's' : ''} (${positionBreakdown})</p>
</div>
<div> const playersDiv = document.createElement("div");
<h6 class="text-xl font-semibold text-gray-800 mb-3">Liste des joueurs:</h6> const playersH6 = document.createElement("h6");
<ul class="grid grid-cols-1 md:grid-cols-2 gap-3"> playersH6.className = "text-xl font-semibold text-gray-800 mb-3";
${playersByPosition.map(player => { playersH6.textContent = "Liste des joueurs:";
let number = player.number ? player.number : "N/A"; playersDiv.appendChild(playersH6);
let position = player.position ? player.position : "N/A"; const playersUl = document.createElement("ul");
return `<li class="bg-indigo-50 rounded-lg p-3"><span class="font-medium">[${position}] ${number}</span> - ${player.fname} ${player.lname} <span class="text-gray-500 text-sm">(${player.dob})</span></li>`; playersUl.className = "grid grid-cols-1 md:grid-cols-2 gap-3";
}).join('')} playersByPosition.forEach(player => {
</ul> const li = document.createElement("li");
</div> li.className = "bg-indigo-50 rounded-lg p-3";
const number = player.number ? player.number : "N/A";
${staffHtml} const position = player.position ? player.position : "N/A";
</div> 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"); eventDetailsModal.classList.remove("hidden");
}) })