fix: XSS vulnerabilities and error handling in import window

- Escape creators, year, item_type fields in renderImportItemList and
  showItemPreview (was only escaping title and abstract)
- loadImportState uses api() wrapper for proper error handling
- generateSelectedCards uses api() wrapper with try/catch
- Remove duplicate isImportWindowOpen in state object

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Saberlve 2026-04-15 20:34:42 +08:00
parent aae8a6340a
commit c6e1d20c68

View File

@ -495,7 +495,6 @@
currentCards: [], currentCards: [],
collectionTree: [], collectionTree: [],
isImportWindowOpen: false, isImportWindowOpen: false,
isImportWindowOpen: false,
windowX: typeof windowSessionState.x === "number" ? windowSessionState.x : null, windowX: typeof windowSessionState.x === "number" ? windowSessionState.x : null,
windowY: typeof windowSessionState.y === "number" ? windowSessionState.y : null, windowY: typeof windowSessionState.y === "number" ? windowSessionState.y : null,
windowWidth: typeof windowSessionState.width === "number" ? windowSessionState.width : null, windowWidth: typeof windowSessionState.width === "number" ? windowSessionState.width : null,
@ -1140,8 +1139,7 @@
} }
async function loadImportState() { async function loadImportState() {
const resp = await fetch(`/api/projects/${state.currentProjectId}/import-state`); const data = await api(`/api/projects/${state.currentProjectId}/import-state`);
const data = await resp.json();
state.importItems = data.items; state.importItems = data.items;
renderImportItemList(); renderImportItemList();
updateImportCounts(data.pending_count, data.done_count); updateImportCounts(data.pending_count, data.done_count);
@ -1159,7 +1157,7 @@
<input type="checkbox" ${isDone ? 'disabled' : ''} data-key="${item.item_key}"> <input type="checkbox" ${isDone ? 'disabled' : ''} data-key="${item.item_key}">
<div> <div>
<div class="item-title">${escapeHtml(item.title)}</div> <div class="item-title">${escapeHtml(item.title)}</div>
<div class="item-meta">${item.creators.join(', ')} · ${item.year || 'n.d.'}</div> <div class="item-meta">${escapeHtml(item.creators.join(', '))} · ${escapeHtml(item.year || 'n.d.')}</div>
</div> </div>
`; `;
if (!isDone) { if (!isDone) {
@ -1178,7 +1176,7 @@
function showItemPreview(item) { function showItemPreview(item) {
document.getElementById('item-preview').innerHTML = ` document.getElementById('item-preview').innerHTML = `
<div class="preview-title">${escapeHtml(item.title)}</div> <div class="preview-title">${escapeHtml(item.title)}</div>
<div class="preview-meta">${item.creators.join(', ')} · ${item.year || 'n.d.'} · ${item.item_type}</div> <div class="preview-meta">${escapeHtml(item.creators.join(', '))} · ${escapeHtml(item.year || 'n.d.')} · ${escapeHtml(item.item_type)}</div>
<div class="preview-abstract">${escapeHtml(item.abstract || 'No abstract available.')}</div> <div class="preview-abstract">${escapeHtml(item.abstract || 'No abstract available.')}</div>
`; `;
} }
@ -1200,12 +1198,10 @@
updateGenerateButton(); updateGenerateButton();
try { try {
const resp = await fetch(`/api/projects/${state.currentProjectId}/cards/generate`, { const data = await api(`/api/projects/${state.currentProjectId}/cards/generate`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ item_keys: keys }), body: JSON.stringify({ item_keys: keys }),
}); });
const data = await resp.json();
data.items.forEach(updated => { data.items.forEach(updated => {
const item = state.importItems.find(i => i.item_key === updated.item_key); const item = state.importItems.find(i => i.item_key === updated.item_key);
@ -1220,6 +1216,8 @@
done: state.importItems.filter(i => i.card_status === 'done').length, done: state.importItems.filter(i => i.card_status === 'done').length,
}; };
updateImportCounts(counts.pending, counts.done); updateImportCounts(counts.pending, counts.done);
} catch (error) {
setStatus(elements.windowCollectionStatus, error.message, true);
} finally { } finally {
state.isGenerating = false; state.isGenerating = false;
updateGenerateButton(); updateGenerateButton();