Whitelist research source links (#2499)

This commit is contained in:
Vykos
2026-06-04 20:41:35 +02:00
committed by GitHub
parent ed933ac232
commit 3ae89599f3
3 changed files with 50 additions and 5 deletions

View File

@@ -76,6 +76,15 @@ function _hlSearch(text) {
'<mark class="doclib-search-hl">$1</mark>');
} catch { return esc; }
}
function _safeResearchHref(raw) {
try {
const parsed = new URL(String(raw || '').trim(), window.location.origin);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return _esc(parsed.href);
} catch {}
return '';
}
let _libraryEscHandler = null;
let _librarySelectMode = false;
let _librarySelectedIds = new Set();
@@ -2649,7 +2658,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
const data = await res.json();
_researchItems = data.research || data || [];
} catch (e) {
grid.innerHTML = `<div class="hwfit-loading">Failed to load: ${e.message}</div>`;
grid.innerHTML = `<div class="hwfit-loading">Failed to load: ${_esc(e.message)}</div>`;
return;
}
_renderResearchGrid();
@@ -2691,9 +2700,9 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
const sources = Array.isArray(detail.sources) ? detail.sources : [];
const sourcesList = sources.slice(0, 12).map((src, i) => {
const title = _esc(src.title || src.url || `Source ${i + 1}`);
const url = src.url || '';
const url = _safeResearchHref(src.url);
return url
? `<li><a href="${_esc(url)}" target="_blank" rel="noopener">${title}</a></li>`
? `<li><a href="${url}" target="_blank" rel="noopener">${title}</a></li>`
: `<li>${title}</li>`;
}).join('');
const sourcesHtml = sources.length

View File

@@ -1103,8 +1103,10 @@ function _renderResult(job) {
html += '<div class="research-job-sources">';
for (const s of job.sources.slice(0, 10)) {
const title = _esc(s.title || s.url || '');
const url = _esc(s.url || '');
html += `<a href="${url}" target="_blank" rel="noopener" class="research-source-link">${title}</a>`;
const url = _safeSourceHref(s.url);
html += url
? `<a href="${url}" target="_blank" rel="noopener" class="research-source-link">${title}</a>`
: `<span class="research-source-link">${title}</span>`;
}
if (job.sources.length > 10) html += `<span class="research-source-more">+${job.sources.length - 10} more</span>`;
html += '</div>';
@@ -1231,3 +1233,11 @@ function _esc(s) {
d.textContent = s || '';
return d.innerHTML;
}
function _safeSourceHref(raw) {
try {
const parsed = new URL(String(raw || '').trim(), window.location.origin);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return _esc(parsed.href);
} catch {}
return '';
}

View File

@@ -0,0 +1,26 @@
"""Regression guards for API-provided research source hrefs."""
from pathlib import Path
_REPO = Path(__file__).resolve().parent.parent
def test_document_library_research_preview_whitelists_source_hrefs():
src = (_REPO / "static" / "js" / "documentLibrary.js").read_text(encoding="utf-8")
assert "function _safeResearchHref(raw)" in src
assert "parsed.protocol === 'http:' || parsed.protocol === 'https:'" in src
assert "const url = _safeResearchHref(src.url);" in src
assert 'href="${_esc(url)}"' not in src
assert "Failed to load: ${_esc(e.message)}" in src
assert "Failed to load: ${e.message}" not in src
def test_research_panel_whitelists_source_hrefs():
src = (_REPO / "static" / "js" / "research" / "panel.js").read_text(encoding="utf-8")
assert "function _safeSourceHref(raw)" in src
assert "parsed.protocol === 'http:' || parsed.protocol === 'https:'" in src
assert "const url = _safeSourceHref(s.url);" in src
assert 'const url = _esc(s.url || \'\');' not in src