From fd9daef40baee328f514c6e745fda399f07721c8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:41:34 +0000 Subject: [PATCH] [security] Remove innerHTML usage from searchView.js Refactored `js/searchView.js` to use safe DOM APIs (`document.createElement`, `replaceChildren`, `textContent`) instead of `innerHTML`. This reduces the attack surface for XSS vulnerabilities. Updated `scripts/check-innerhtml.mjs` baseline. Verified with unit tests and a frontend verification script. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/searchView.js | 82 ++++++++++++++++++++------------ scripts/check-innerhtml.mjs | 1 - scripts/verify-search.py | 48 +++++++++++++++++++ verification/error.png | Bin 0 -> 300080 bytes verification/search-results.png | Bin 0 -> 36669 bytes 5 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 scripts/verify-search.py create mode 100644 verification/error.png create mode 100644 verification/search-results.png diff --git a/js/searchView.js b/js/searchView.js index 7f9422d6..0a9e92a9 100644 --- a/js/searchView.js +++ b/js/searchView.js @@ -54,9 +54,17 @@ const extractRelayHints = (event) => { ); }; +// Helper to create message elements for empty/loading/error states +function createMessageElement(text, className) { + const p = document.createElement('p'); + p.className = className || "text-sm text-muted col-span-full"; + p.textContent = text; + return p; +} + function renderProfileCards(profiles, container) { if (!container) return; - container.innerHTML = ""; + container.replaceChildren(); // Reset the "Show More" button and container limit state const showMoreContainer = document.getElementById("searchChannelShowMore"); @@ -66,7 +74,7 @@ function renderProfileCards(profiles, container) { container.classList.remove("channel-grid-limit"); if (!profiles || profiles.length === 0) { - container.innerHTML = `
No matching channels found.
`; + container.appendChild(createMessageElement("No matching channels found.")); return; } @@ -93,7 +101,7 @@ function renderProfileCards(profiles, container) { const metadata = profile.metadata || {}; const name = metadata.name || metadata.display_name || "Unknown User"; const picture = sanitizeProfileMediaUrl(metadata.picture) || FALLBACK_AVATAR; - const about = metadata.about || ""; + // const about = metadata.about || ""; // Unused in original code let npub = ""; try { @@ -110,25 +118,37 @@ function renderProfileCards(profiles, container) { card.href = `#view=channel-profile&npub=${npub || pubkey}`; card.className = "flex items-center gap-3 p-3 rounded-lg bg-surface hover:bg-surface-elevated transition-colors border border-border group"; - card.innerHTML = ` -- ${escapeHTML(shortNpub)} -
-Searching channels...
`; + channelList.replaceChildren(createMessageElement("Searching channels...", "text-sm text-muted animate-pulse col-span-full")); } if (videoList) { - videoList.innerHTML = `Searching videos...
`; + videoList.replaceChildren(createMessageElement("Searching videos...", "text-sm text-muted animate-pulse col-span-full")); } if (!query) { - if (channelList) channelList.innerHTML = `Please enter a search term.
`; - if (videoList) videoList.innerHTML = `Please enter a search term.
`; + if (channelList) channelList.replaceChildren(createMessageElement("Please enter a search term.")); + if (videoList) videoList.replaceChildren(createMessageElement("Please enter a search term.")); return; } @@ -185,7 +205,7 @@ export async function initSearchView() { }).catch(err => { devLogger.warn("[Search] Profile search failed", err); if (channelList && searchToken === currentSearchToken) { - channelList.innerHTML = `Failed to load channels.
`; + channelList.replaceChildren(createMessageElement("Failed to load channels.", "text-sm text-critical col-span-full")); } }); @@ -197,7 +217,7 @@ export async function initSearchView() { }).catch(err => { devLogger.warn("[Search] Video search failed", err); if (videoList && searchToken === currentSearchToken) { - videoList.innerHTML = `Failed to load videos.
`; + videoList.replaceChildren(createMessageElement("Failed to load videos.", "text-sm text-critical col-span-full")); } }); } @@ -255,7 +275,7 @@ function renderActiveFilters(filters) { return; } - list.innerHTML = ""; + list.replaceChildren(); const pills = []; filters.authorPubkeys?.forEach((author) => { @@ -413,10 +433,10 @@ function renderActiveFilters(filters) { async function renderSearchVideos(videos, container, app) { if (!container) return; - container.innerHTML = ""; + container.replaceChildren(); if (!videos || videos.length === 0) { - container.innerHTML = `No matching videos found.
`; + container.appendChild(createMessageElement("No matching videos found.")); return; } diff --git a/scripts/check-innerhtml.mjs b/scripts/check-innerhtml.mjs index d6ced3d1..f4890bf5 100644 --- a/scripts/check-innerhtml.mjs +++ b/scripts/check-innerhtml.mjs @@ -31,7 +31,6 @@ const BASELINE = { "js/historyView.js": 4, "js/index.js": 1, "js/kidsView.js": 1, - "js/searchView.js": 12, "js/subscriptions.js": 7, "js/ui/components/DeleteModal.js": 3, "js/ui/components/EditModal.js": 1, diff --git a/scripts/verify-search.py b/scripts/verify-search.py new file mode 100644 index 00000000..bd7c170e --- /dev/null +++ b/scripts/verify-search.py @@ -0,0 +1,48 @@ +from playwright.sync_api import sync_playwright +import os + +def run(playwright): + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + + # Inject localStorage to bypass disclaimer + page = context.new_page() + page.add_init_script(""" + localStorage.setItem("hasSeenDisclaimer", "true"); + """) + + # Capture console logs + page.on("console", lambda msg: print(f"Browser console: {msg.text}")) + + try: + # Navigate to the correct search URL format + page.goto("http://localhost:3000/#view=search&q=test", wait_until="networkidle") + print("Navigated to page") + + # Wait for #searchTitle + print("Waiting for #searchTitle") + page.wait_for_selector("#searchTitle", timeout=10000) + print("Found #searchTitle") + + # Wait for results or "no results" message + try: + # My refactor uses with text content
+ page.wait_for_selector("#searchVideoList p", timeout=10000)
+ print("Found search results message")
+ except:
+ print("Timeout waiting for search results message")
+
+ # Take a screenshot
+ os.makedirs("verification", exist_ok=True)
+ page.screenshot(path="verification/search-results.png")
+ print("Screenshot saved")
+
+ except Exception as e:
+ print(f"Error: {e}")
+ page.screenshot(path="verification/error.png")
+
+ finally:
+ browser.close()
+
+with sync_playwright() as playwright:
+ run(playwright)
diff --git a/verification/error.png b/verification/error.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac4268ce8a72c9914734061f2c92086b9c4f826e
GIT binary patch
literal 300080
zcmXtfV|<;_^7Tp1iEZ1qZQDj;G-lJ}gpF<6wr#YrohFT&q%q&zd*A pSWJ#Red&v?((d^VVoI!0$o}K-(fUpHwVQ``OYTLc4^x7MjfE wwV(hOk0go2hFt+IAx<=qip?w%YVX)wEc{F1CT!uzS{*4w
zsrTX@0s;H!#FM~goBrCc2F-k~Yy8B}NcsjgU0J8STyDcjs($9|mZuC8Yi)7dIw0ga
zZVHFLL|ylcOyhHoO23zgSY~Jqcb@knUT~5%wI9mWXXYc`&+z385ki1MFnVHbXC2JV_;3C
zW;dY!paN&9-KG~o!;BIXusJR7+uwsXLbEw$x4@%ARWa8xDwW6!JAC5YgdM)hqnFry
z$EP!f>2lD?2v?qMGwx_}QdxOoTd*_4NBN57fQ%risM6)f;UjB8K$H1cXIrB38M956
z4bPeB(Pq9vyG{N86QZ*SR1--Y^-CzHlUCJa0qcs|5xn08W5y*=U-*Cmg$7i4u~OW-
z%dx695i-In!J7w77EB0*6kA8}>j=a2DyhJ>fN?Bb55%`JoRVCvd^3kvQFu+J%?Vf9
zt)^5Deaqis;T_=`R5KuncwBf5ARDcHI+sZ&;5;x^3pWUhZ)-4LlnoaT4yDuO!<)L^
zuJc973|vi+KmC(VGV>>oX~Y}&_EmvaMTqtL!e7T{_mU^|BygseN8#|YZDnDnwP$3E
z8VoGmUz=bsfCTJWvwZ-ENZydW5UEF(nJZ6$m~TrV`Iim?!U7wTPgB*!>Ez~EBC$md
zLkgq#u^6jTH|+Xyczr^*Tjk_OG%cRgD4~~F$`)H!DvaH)Gb?K9*
~sl)wa+6l`=fjZk$+U27&2>Jkv_saKv2WSz^r%wYT{wq`r3Z?xOoBFMKPkGM)p
zu#8da@_h|&$b(PFZC?i)5J!jbe4(k#!m5ZlBEH0WqbL=v)r9_2h&&dlCm&?buAGN;
z^@<+(7ugKCd9fK0m<_WsYgtPEfQlQMp{M3*Cgm|PS6(uxqsyzA@IwiQ5w2g;(;H3Q
zxI;3j(UU_8+Wxmqz!(<&t10=DX4H5o#-CWmxZ->ftVq=5niAYSP57(SDc72Gyl4d<
zSC*W63&j?pA?H7Yp
2&lrKFf*U)h
zG-+7jlQh}fuH4~NeO{ol4|B#R@p+#%u`m83ZhjPY%o{xMB#vdnx3U*;sgNXPQ61w(
z(HuYNrfQMcPCs-7uW+?*Uq6ZiBb~Ni*DdzpjLIxRskeJ8x$A_Gn}p3){>KJ?
cwdQk
z5IZ3Aj7DR>v98otnC*e4rei`418u5WdbB^5F|V-p$ORgHAQI2kqXd~w%iqYZYCxh6
zI4b`H5H