From a313db1bd8e1f4ec512c0d34b11c8eca274c2d19 Mon Sep 17 00:00:00 2001 From: Vagarth Date: Tue, 24 Feb 2026 02:24:12 +0530 Subject: [PATCH] fix: replace innerHTML with safe DOM APIs to prevent XSS --- js/forum-fetch.js | 62 +++++++++++++++++++++++++++++++------------- js/github-queries.js | 47 ++++++++++++++++++++++++++------- js/news-collect.js | 39 ++++++++++++++++++++++++---- 3 files changed, 115 insertions(+), 33 deletions(-) diff --git a/js/forum-fetch.js b/js/forum-fetch.js index c73124348a2..403823cc92d 100644 --- a/js/forum-fetch.js +++ b/js/forum-fetch.js @@ -12,6 +12,15 @@ document.addEventListener("DOMContentLoaded", async function () { loadingText.textContent = "Loading FAQs..."; list.parentElement.insertBefore(loadingText, list); + function isSafeUrl(url) { + try { + var parsed = new URL(url, window.location.origin); + return parsed.protocol === "https:" || parsed.protocol === "http:"; + } catch (e) { + return false; + } + } + try { const res = await fetch("/assets/data/faq.json"); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -50,7 +59,7 @@ document.addEventListener("DOMContentLoaded", async function () { } function renderTopics() { - list.innerHTML = ""; + list.replaceChildren(); const shown = filteredTopics.slice(0, visibleCount); @@ -72,22 +81,39 @@ document.addEventListener("DOMContentLoaded", async function () { ? t.excerpt : "No description available."; - card.innerHTML = ` -

- ${t.title} -

-

- ${excerpt} - - Read more - -

-

- Last updated: ${new Date(t.last_posted_at).toLocaleDateString("en-GB")} | - Replies: ${t.posts_count} | Views: ${t.views} -

- `; + var h4 = document.createElement("h4"); + h4.style.cssText = "margin-bottom:8px;"; + var strong = document.createElement("strong"); + strong.textContent = t.title; + h4.appendChild(strong); + + var excerptP = document.createElement("p"); + excerptP.style.cssText = "color:#333;line-height:1.4;"; + excerptP.textContent = excerpt + " "; + var readMore = document.createElement("a"); + if (isSafeUrl(t.url)) { + readMore.setAttribute("href", t.url); + } + readMore.setAttribute("target", "_blank"); + readMore.setAttribute("rel", "noopener noreferrer"); + readMore.style.cssText = "text-decoration:none;color:#0069c2;"; + readMore.dataset.noicon = ""; + readMore.textContent = "Read more"; + excerptP.appendChild(readMore); + + var metaP = document.createElement("p"); + metaP.style.cssText = "color:#666;font-size:0.9em;"; + metaP.textContent = + "Last updated: " + + new Date(t.last_posted_at).toLocaleDateString("en-GB") + + " | Replies: " + + t.posts_count + + " | Views: " + + t.views; + + card.appendChild(h4); + card.appendChild(excerptP); + card.appendChild(metaP); list.appendChild(card); }); @@ -103,7 +129,7 @@ document.addEventListener("DOMContentLoaded", async function () { list.appendChild(btn); } - // 🧽 JS-based CSS injection to remove external icon pseudo-element + // JS-based CSS injection to remove external icon pseudo-element if (!document.getElementById("noicon-style")) { const style = document.createElement("style"); style.id = "noicon-style"; diff --git a/js/github-queries.js b/js/github-queries.js index 132f88ffc00..1a8c85a4f80 100644 --- a/js/github-queries.js +++ b/js/github-queries.js @@ -10,7 +10,7 @@ // Get the updated field of an entry of the feed and format it like "Jan 1, 1970" function formatDate(updated) { // Extract the date and create a Date object - date = new Date(updated); + var date = new Date(updated); // Names for the months (we do not need an external library just for this) var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -27,12 +27,23 @@ var github_api_endpoint = 'https://api.github.com/repos/precice/precice/releases fetch(github_api_endpoint).then(function(response) { if (response.ok) { response.json().then(function(data) { - tag = data[0].name; - published_at = data[0].published_at; - url = data[0].html_url - // Format the text, which contains the link, the title, and the date. - var text = 'Latest ' + tag + ' (' + formatDate(published_at) + ')  '; - document.getElementById('latest-release').innerHTML = text; + var tag = data[0].name; + var published_at = data[0].published_at; + var url = data[0].html_url; + + var link = document.createElement('a'); + link.setAttribute('href', url); + link.className = 'btn btn-secondary no-icon action-button'; + link.setAttribute('role', 'button'); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + link.appendChild(document.createTextNode('Latest ' + tag + ' (' + formatDate(published_at) + ') \u00A0')); + var icon = document.createElement('i'); + icon.className = 'fas fa-download'; + link.appendChild(icon); + + var container = document.getElementById('latest-release'); + container.replaceChildren(link); }); } else throw new Error("Problem with fetching releases"); @@ -54,9 +65,25 @@ document.addEventListener("DOMContentLoaded", function() { fetch(github_api_endpoint).then(function(response) { if (response.ok) { response.json().then(function(data) { - count = data.stargazers_count - var text = 'Star on GitHub   ' + count + ''; - document.getElementById('github-button').innerHTML = text; + var count = data.stargazers_count; + + var link = document.createElement('a'); + link.setAttribute('href', 'https://github.com/precice/precice/'); + link.className = 'btn btn-default no-icon action-button'; + link.setAttribute('role', 'button'); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + link.appendChild(document.createTextNode('Star on GitHub \u00A0')); + var icon = document.createElement('i'); + icon.className = 'fas fa-star'; + link.appendChild(icon); + var span = document.createElement('span'); + span.id = 'stargazers'; + span.textContent = ' ' + count; + link.appendChild(span); + + var container = document.getElementById('github-button'); + container.replaceChildren(link); }); } else throw new Error("Problem with fetching stargazers"); diff --git a/js/news-collect.js b/js/news-collect.js index 28d2abcb662..a00749dd249 100644 --- a/js/news-collect.js +++ b/js/news-collect.js @@ -2,6 +2,15 @@ document.addEventListener("DOMContentLoaded", async function () { const newsContainer = document.getElementById("news-container"); const loadingText = document.getElementById("loading-news"); + function isSafeUrl(url) { + try { + var parsed = new URL(url, window.location.origin); + return parsed.protocol === "https:" || parsed.protocol === "http:"; + } catch (e) { + return false; + } + } + try { const response = await fetch("/assets/data/news.json"); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -22,12 +31,32 @@ document.addEventListener("DOMContentLoaded", async function () { const date = new Date(topic.created_at || topic.last_posted_at).toLocaleDateString("en-GB"); - card.innerHTML = ` -

${topic.title}

-

${topic.description}

-

${date}

-
`; + const link = document.createElement("a"); + if (isSafeUrl(topic.url)) { + link.setAttribute("href", topic.url); + } + link.setAttribute("target", "_blank"); + link.setAttribute("rel", "noopener noreferrer"); + link.className = "news-link no-external-marker"; + + const h4 = document.createElement("h4"); + const strong = document.createElement("strong"); + strong.textContent = topic.title; + h4.appendChild(strong); + + const descP = document.createElement("p"); + descP.textContent = topic.description; + + const dateP = document.createElement("p"); + dateP.className = "text-muted"; + const small = document.createElement("small"); + small.textContent = date; + dateP.appendChild(small); + link.appendChild(h4); + link.appendChild(descP); + link.appendChild(dateP); + card.appendChild(link); col.appendChild(card); newsContainer.appendChild(col); }