diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7b41c36d6..9b1e5b5f9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -431,6 +431,7 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-xss-333-pages-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-xss-392-perf-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-xss-368-home-links-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-xss-429-home-footer-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1639-observers-sort-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-405-observers-listener-leak-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1758-ng-filter-rerenders-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/customize-v2.js b/public/customize-v2.js index 45f044598..6bae00fc3 100644 --- a/public/customize-v2.js +++ b/public/customize-v2.js @@ -68,6 +68,8 @@ function _setBrandLogoUrl(url, alt) { var node = document.querySelector('.nav-brand .brand-logo'); if (!node) return; + // #429 — a URL the shared scheme check rejects is treated as cleared. + if (url && !_brandUrlOk(url)) url = ''; if (url) { if (node.tagName.toLowerCase() === 'img') { node.setAttribute('src', url); @@ -97,6 +99,17 @@ if (alt != null) node.setAttribute('alt', alt); } } + // #429 — branding URLs are customization content (server theme, stored + // overrides, theme import), so they get the scheme check roles.js exports + // for miniMarkdown and Home footer links, in its image-sink form. Fails + // closed when roles.js is not loaded. + function _brandUrlOk(url) { + return typeof window.isSafeImageUrl === 'function' && window.isSafeImageUrl(url); + } + function _setFaviconUrl(url) { + var link = document.querySelector('link[rel="icon"]'); + if (link && url && _brandUrlOk(url)) link.href = url; + } function _setBrandAlt(alt) { var node = document.querySelector('.nav-brand .brand-logo'); if (!node) return; @@ -795,15 +808,8 @@ var brandEl = document.querySelector('.brand-text'); if (brandEl) brandEl.textContent = br.siteName; } - if (br.logoUrl) { - _setBrandLogoUrl(br.logoUrl, br.siteName || null); - var iconEl = document.querySelector('.brand-icon'); - if (iconEl) iconEl.innerHTML = ''; - } - if (br.faviconUrl) { - var fav = document.querySelector('link[rel="icon"]'); - if (fav) fav.href = br.faviconUrl; - } + if (br.logoUrl) _setBrandLogoUrl(br.logoUrl, br.siteName || null); + if (br.faviconUrl) _setFaviconUrl(br.faviconUrl); // #1518 — apply validated homeUrl override to .nav-brand[href]. // Always call: invalid/empty restores '#/' so a cleared override visibly // reverts. Validator rejects javascript:/data:/etc to prevent XSS. @@ -1301,7 +1307,7 @@ function _renderBranding() { var eff = _getEffective(); var b = eff.branding || {}; - var logoPreview = b.logoUrl ? 'Logo preview' : ''; + var logoPreview = b.logoUrl && _brandUrlOk(b.logoUrl) ? 'Logo preview' : ''; return '
' + '
' + '
' + @@ -2404,15 +2410,9 @@ } if (section === 'branding' && key === 'logoUrl') { _setBrandLogoUrl(inp.value || '', null); - var iconEl = document.querySelector('.brand-icon'); - if (iconEl) { - if (inp.value) iconEl.innerHTML = ''; - else iconEl.innerHTML = ''; - } } if (section === 'branding' && key === 'faviconUrl') { - var link = document.querySelector('link[rel="icon"]'); - if (link && inp.value) link.href = inp.value; + _setFaviconUrl(inp.value); } if (section === 'branding' && key === 'homeUrl') { // #1518 — live nav-brand[href] swap. Validator silently drops @@ -2766,13 +2766,8 @@ } if (overrides.branding.logoUrl) { _setBrandLogoUrl(overrides.branding.logoUrl, overrides.branding.siteName || null); - var iconEl = document.querySelector('.brand-icon'); - if (iconEl) iconEl.innerHTML = ''; - } - if (overrides.branding.faviconUrl) { - var link = document.querySelector('link[rel="icon"]'); - if (link) link.href = overrides.branding.faviconUrl; } + if (overrides.branding.faviconUrl) _setFaviconUrl(overrides.branding.faviconUrl); // #1518 — re-apply homeUrl override after DOM is ready (handles cold // page loads where the customizer pipeline ran before .nav-brand mounted). if (overrides.branding.homeUrl) { diff --git a/public/home.js b/public/home.js index 7e35be568..761008659 100644 --- a/public/home.js +++ b/public/home.js @@ -25,6 +25,15 @@ if (m) return _renderHomeGlyph(m[1]) + ' ' + _escHtml(m[2]); return _escHtml(s); // EMOJI-OK-LEGACY-RENDER } + // #429 — footerLinks come from importable Home customization, so the URL + // gets the same scheme check as miniMarkdown links (roles.js). A rejected + // URL renders as its label text. + function footerLinkHtml(l) { + const href = window.safeUrlAttr(l.url); + const label = _renderHomeLabel(l.label); + if (!href) return label; + return `${label}`; + } function _escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } const PREF_KEY = 'meshcore-user-level'; @@ -149,7 +158,7 @@