fix: frontend URL scheme allowlist, fetch ok-check, image onerror

Add safeUrl() to gate external search-result URLs into href/src (blocks
javascript:/data:), parse the response body before the ok check in apiJson
so non-JSON error bodies surface the real status, and hide broken evidence
preview images via onerror.
This commit is contained in:
유창욱 2026-06-20 18:44:20 +09:00
parent 7f5799e5e1
commit f8aa10f91b

View file

@ -123,11 +123,29 @@ async function apiJson(path, options = {}) {
}); });
const payload = await response.json(); const text = await response.text();
let payload = null;
if (text) {
try {
payload = JSON.parse(text);
} catch (error) {
payload = null;
}
}
if (!response.ok) { if (!response.ok) {
throw new Error(payload.error || `API 요청 실패: ${response.status}`); const message = (payload && payload.error) || `API 요청 실패: ${response.status}`;
throw new Error(message);
} }
@ -523,6 +541,28 @@ function escapeHtml(value) {
function safeUrl(value) {
// Defense-in-depth for URLs derived from external search results: only allow
// absolute http(s) or same-origin paths into href/src (blocks javascript:,
// data:, etc.). The server also normalizes these server-side.
const url = String(value || "").trim();
if (/^https?:\/\//i.test(url) || url.startsWith("/")) {
return url;
}
return "#";
}
function formatReason(reason) { function formatReason(reason) {
const text = String(reason || ""); const text = String(reason || "");
if (!text) return ""; if (!text) return "";
@ -1719,9 +1759,9 @@ function renderEvidencePreview(evidence) {
return ` return `
<a class="evidence-preview" href="${escapeHtml(targetUrl)}" target="_blank" rel="noreferrer" aria-label="검색 이미지 열기"> <a class="evidence-preview" href="${escapeHtml(safeUrl(targetUrl))}" target="_blank" rel="noreferrer" aria-label="검색 이미지 열기">
<img src="${escapeHtml(previewUrl)}" alt=""> <img src="${escapeHtml(safeUrl(previewUrl))}" alt="" onerror="this.style.display='none'">
</a> </a>
@ -1741,7 +1781,7 @@ function renderEvidenceLink(evidence) {
return ` return `
<a class="evidence-link" href="${escapeHtml(url)}" target="_blank" rel="noreferrer"> <a class="evidence-link" href="${escapeHtml(safeUrl(url))}" target="_blank" rel="noreferrer">
${escapeHtml(label)} ${escapeHtml(label)}
@ -1937,7 +1977,7 @@ function renderCollectionCandidates() {
</div> </div>
${ ${
candidate.sourceUrl candidate.sourceUrl
? `<a class="evidence-link" href="${escapeHtml(candidate.sourceUrl)}" target="_blank" rel="noreferrer">출처 열기</a>` ? `<a class="evidence-link" href="${escapeHtml(safeUrl(candidate.sourceUrl))}" target="_blank" rel="noreferrer">출처 열기</a>`
: "" : ""
} }
</div> </div>