From 78909cd308592f11d4bc74d82b81ff412c5662b5 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 10 Apr 2026 17:26:46 +0200 Subject: [PATCH] security: harden quick search dom --- static/js/search.js | 85 +++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/static/js/search.js b/static/js/search.js index e99e801..fce14d3 100644 --- a/static/js/search.js +++ b/static/js/search.js @@ -12,7 +12,7 @@ const query = e.target.value.trim() if (query.length < 2) { - searchDropdown.innerHTML = '' + searchDropdown.replaceChildren() return } @@ -21,26 +21,60 @@ .then(res => res.json()) .then(results => { if (!results || results.length === 0) { - searchDropdown.innerHTML = '' + searchDropdown.replaceChildren() return } - let html = '
' - html += '
Anime
' + const searchResults = document.createElement('div') + searchResults.className = 'search-results' + + const title = document.createElement('div') + title.className = 'search-results-title' + title.textContent = 'Anime' + searchResults.appendChild(title) + results.forEach(r => { - html += ` - - ${r.image ? `${r.title}` : '
no image
'} -
-
${escapeHtml(r.title)}
-
${r.type}
-
-
- ` + const item = document.createElement('a') + item.className = 'search-result-item' + item.setAttribute('href', '/anime/' + encodeURIComponent(String(r.id || ''))) + + if (isSafeImageUrl(r.image)) { + const img = document.createElement('img') + img.className = 'search-result-thumb' + img.setAttribute('src', r.image) + img.setAttribute('alt', String(r.title || '')) + item.appendChild(img) + } else { + const noImage = document.createElement('div') + noImage.className = 'search-result-no-image' + noImage.textContent = 'no image' + item.appendChild(noImage) + } + + const info = document.createElement('div') + info.className = 'search-result-info' + + const itemTitle = document.createElement('div') + itemTitle.className = 'search-result-title' + itemTitle.textContent = String(r.title || '') + info.appendChild(itemTitle) + + const itemType = document.createElement('div') + itemType.className = 'search-result-type' + itemType.textContent = String(r.type || '') + info.appendChild(itemType) + + item.appendChild(info) + searchResults.appendChild(item) }) - html += `View all results for ${escapeHtml(query)}` - html += '
' - searchDropdown.innerHTML = html + + const viewAll = document.createElement('a') + viewAll.className = 'search-result-view-all' + viewAll.setAttribute('href', '/search?q=' + encodeURIComponent(query)) + viewAll.textContent = 'View all results for ' + query + searchResults.appendChild(viewAll) + + searchDropdown.replaceChildren(searchResults) }) .catch(err => console.error('Search error:', err)) }, 300) @@ -48,20 +82,27 @@ searchInput.addEventListener('blur', () => { setTimeout(() => { - searchDropdown.innerHTML = '' + searchDropdown.replaceChildren() }, 200) }) document.addEventListener('click', (e) => { if (!e.target.closest('.header-search-wrapper')) { - searchDropdown.innerHTML = '' + searchDropdown.replaceChildren() } }) } - function escapeHtml(text) { - const div = document.createElement('div') - div.textContent = text - return div.innerHTML + function isSafeImageUrl(rawUrl) { + if (!rawUrl || typeof rawUrl !== 'string') { + return false + } + + try { + const parsed = new URL(rawUrl, window.location.origin) + return parsed.protocol === 'https:' || parsed.protocol === 'http:' + } catch { + return false + } } })()