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:
parent
7f5799e5e1
commit
f8aa10f91b
1 changed files with 46 additions and 6 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue