From a8e7afda3fdbd3caadc4b0e29263360a2f138a27 Mon Sep 17 00:00:00 2001 From: WofWca Date: Mon, 23 Feb 2026 12:08:05 +0400 Subject: [PATCH] fix: XSS through unsanitized app name The `escapeHtml` implementation is not adequate for escaping attribute values. Let's just not be using `innerHTML` at all. The issue has been introduced in b32a83eab60139da0d90b67ab42c01328365de69 (https://github.com/webxdc/website/pull/119). Seems that anyone who has an app submitted in the store can exploit this vulnerability, by making a release with a malicious app name. I have tested this. Things seem to be working as before. --- website/assets/index.js | 38 +++++++++++++++++++++++--------------- website/index.html | 10 ++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/website/assets/index.js b/website/assets/index.js index c1b0689..7085c4f 100644 --- a/website/assets/index.js +++ b/website/assets/index.js @@ -53,12 +53,6 @@ var xdcgetBase = 'https://apps.testrun.org'; var appMap = {}; - function escapeHtml(str) { - var div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - function extractAuthor(url) { var m = /(github\.com|codeberg\.org)\/([\w\-_]+)/.exec(url || ''); return m && m.length === 3 ? m[2] : ''; @@ -94,21 +88,35 @@ else idx++; } - var html = ''; + var templateEl = document.getElementById('carousel-card-template'); shuffled.forEach(function(app) { appMap[app.app_id] = app; + /** @type {HTMLButtonElement} */ + var el = templateEl.cloneNode(true); + el.removeAttribute('id'); var subtitle = (app.description || '').split('\n')[0]; var author = app._author; var searchText = (app.name + ' ' + (app.description || '') + ' ' + author).toLowerCase(); - html += ''; + + el.setAttribute('data-app-id', app.app_id); + el.setAttribute('data-search', searchText); + el.ariaLabel = app.name; + var img = el.getElementsByTagName('img')[0]; + img.src = xdcgetBase + '/' + app.icon_relname; + img.alt = app.name + ' icon'; + el.getElementsByClassName('carousel-card-name')[0].textContent = app.name; + el.getElementsByClassName('carousel-card-desc')[0].textContent = subtitle; + el.getElementsByClassName('carousel-card-author')[0].textContent = author; + if (!author) { + el.getElementsByClassName('carousel-card-author')[0].remove(); + } + + track.append(el); }); - html += ''; - track.innerHTML = html; + var unmatchedEl = document.createElement('div'); + unmatchedEl.id = 'carousel-unmatched'; + unmatchedEl.classList.add('carousel-unmatched'); + track.append(unmatchedEl); // update search placeholder with app count var searchEl = document.getElementById('carousel-search-input'); diff --git a/website/index.html b/website/index.html index bcfc815..cf2bd6f 100644 --- a/website/index.html +++ b/website/index.html @@ -94,6 +94,16 @@

+
+ +