Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 18 additions & 23 deletions public/customize-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '<img src="' + br.logoUrl + '" style="height:24px" onerror="this.style.display=\'none\'">';
}
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.
Expand Down Expand Up @@ -1301,7 +1307,7 @@
function _renderBranding() {
var eff = _getEffective();
var b = eff.branding || {};
var logoPreview = b.logoUrl ? '<img class="cust-preview-img" src="' + escAttr(b.logoUrl) + '" alt="Logo preview" onerror="this.style.display=\'none\'">' : '';
var logoPreview = b.logoUrl && _brandUrlOk(b.logoUrl) ? '<img class="cust-preview-img" src="' + escAttr(b.logoUrl) + '" alt="Logo preview" onerror="this.style.display=\'none\'">' : '';
return '<div class="cust-panel' + (_activeTab === 'branding' ? ' active' : '') + '" data-panel="branding">' +
'<div class="cust-field"><label>Site Name' + _overrideDot('branding', 'siteName') + '</label><input type="text" data-cv2-field="branding.siteName" value="' + escAttr(b.siteName || '') + '"></div>' +
'<div class="cust-field"><label>Tagline' + _overrideDot('branding', 'tagline') + '</label><input type="text" data-cv2-field="branding.tagline" value="' + escAttr(b.tagline || '') + '"></div>' +
Expand Down Expand Up @@ -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 = '<img src="' + inp.value + '" style="height:24px" onerror="this.style.display=\'none\'">';
else iconEl.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg>';
}
}
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
Expand Down Expand Up @@ -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 = '<img src="' + overrides.branding.logoUrl + '" style="height:24px" onerror="this.style.display=\'none\'">';
}
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) {
Expand Down
11 changes: 10 additions & 1 deletion public/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<a href="${href}" class="home-footer-link" target="_blank" rel="noopener">${label}</a>`;
}
function _escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

const PREF_KEY = 'meshcore-user-level';
Expand Down Expand Up @@ -149,7 +158,7 @@

<section class="home-footer">
<div class="home-footer-links">
${homeCfg?.footerLinks ? homeCfg.footerLinks.map(l => `<a href="${escapeAttr(l.url)}" class="home-footer-link" target="_blank" rel="noopener">${_renderHomeLabel(l.label)}</a>`).join('') : `
${homeCfg?.footerLinks ? homeCfg.footerLinks.map(footerLinkHtml).join('') : `
<a href="#/packets" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-package"/></svg> Packets</a>
<a href="#/map" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> Network Map</a>
<a href="#/live" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Live</a>
Expand Down
46 changes: 37 additions & 9 deletions public/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -1047,19 +1047,47 @@
/* RFC 3986 scheme grammar, lowercased before matching. */
var LINK_SCHEME_RE = /^([a-z][a-z0-9+.\-]*):/;

/* True when `url` may become a link or resource URL: it carries no scheme
(relative path, #fragment, ?query) or an allowlisted one. Normalisation
mirrors what a browser does before it resolves a scheme: ASCII controls
and ASCII whitespace are ignored, and the scheme is case-insensitive. So
`JaVa\tScRiPt:alert(1)` is rejected for the same reason
`javascript:alert(1)` is. #429: exported as the one scheme check for
every customization-fed URL sink (Home footer links, branding logo and
favicon), so no sink grows an allowlist of its own. */
function linkUrlProbe(url) {
return String(url == null ? '' : url).replace(/[\u0000-\u0020\u007f]/g, '').toLowerCase();
}
function isSafeLinkUrl(url) {
var probe = linkUrlProbe(url);
if (!probe) return false;
var m = LINK_SCHEME_RE.exec(probe);
return !m || SAFE_LINK_SCHEMES.indexOf(m[1]) !== -1;
}
window.isSafeLinkUrl = isSafeLinkUrl;
/* #429 — image sinks (branding logo, favicon) take the same check plus
inline data:image/ URLs: an <img> or icon never runs script from its URL,
and the customizer has always accepted an inline logo. */
window.isSafeImageUrl = function(url) {
return isSafeLinkUrl(url) || linkUrlProbe(url).indexOf('data:image/') === 0;
};

/* Returns the escaped href for `dest`, or '' when the destination must not
become a link. Normalisation mirrors what a browser does before it
resolves a scheme: ASCII controls and ASCII whitespace are ignored, and
the scheme is case-insensitive. So `JaVa\tScRiPt:alert(1)` is rejected for
the same reason `javascript:alert(1)` is. */
become a link. `dest` is already & < > escaped (see miniMarkdown). */
function safeLinkHref(dest) {
var raw = String(dest == null ? '' : dest);
var probe = raw.replace(/[\u0000-\u0020\u007f]/g, '').toLowerCase();
if (!probe) return '';
var m = LINK_SCHEME_RE.exec(probe);
if (m && SAFE_LINK_SCHEMES.indexOf(m[1]) === -1) return '';
return escapeAttr(raw);
return isSafeLinkUrl(raw) ? escapeAttr(raw) : '';
}

/* #429 — the same check for a RAW url headed for a quoted HTML attribute:
returns the fully escaped value (& < > " '), or '' when it must not
become a link. '&' matters here: left alone, `&#106;avascript:` carries
no scheme for the check but is decoded back into `javascript:` while the
browser parses the attribute. */
window.safeUrlAttr = function(url) {
return safeLinkHref(String(url == null ? '' : url)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
};
// Simple markdown → HTML (bold, italic, links, code, lists, line breaks)
window.miniMarkdown = function(text) {
if (!text) return '';
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ run test-issue-1107-live-layout.js
run test-issue-1619-feed-detail-card-draggable.js
run test-xss-escape-sinks.js
run test-issue-368-minimarkdown-links.js
run test-issue-429-config-url-schemes.js
run test-issue-393-slowpath-doc-bound.js
run test-preflight-xss-gate.js
run test-issue-1648-m4-emoji-scan.js
Expand Down
Loading
Loading