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:
+170
-118
@@ -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");
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user