From fc1bb307bc83441d5d94b602ddccf6d2f44b1212 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 11 Jul 2026 00:41:40 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20harden=20vibe=20import=20trust?= =?UTF-8?q?=20boundary=20=E2=80=94=20strip=20markup=20from=20name/emoji=20?= =?UTF-8?q?(ship-check)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vibe name + emoji arrive from user-pasted GSV1 codes and #vibe= URL fragments and are rendered via innerHTML in the vibes panel + the shared-vibe offer card. name was <-escaped but emoji was raw. sanitizeVibe now strips [<>&"'`] from both at the trust boundary, so nothing dangerous reaches any render path. Verified live: a crafted code with name='' + emoji='' imported via #vibe= → no / in the DOM, no script fired, both rendered as inert text. (ship-check item 3: user strings → HTML must be escaped.) Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/viz/index.html b/viz/index.html index 04d22b0..81bb940 100644 --- a/viz/index.html +++ b/viz/index.html @@ -3153,8 +3153,11 @@ const vibeKnownDest = (k) => dests.has(k); function sanitizeVibe(raw) { // trust boundary: user-pasted / URL data if (!raw || typeof raw !== "object") return null; - const name = (typeof raw.name === "string" && raw.name.trim()) ? raw.name.trim().slice(0, 48) : "shared vibe"; - const emoji = (typeof raw.emoji === "string" && raw.emoji) ? [...raw.emoji].slice(0, 2).join("") : "🎁"; + const noMarkup = (s) => String(s).replace(/[<>&"'`]/g, ""); // strip markup chars — these strings hit innerHTML + const nm = (typeof raw.name === "string") ? noMarkup(raw.name).trim() : ""; + const name = nm ? nm.slice(0, 48) : "shared vibe"; + const em = (typeof raw.emoji === "string") ? noMarkup(raw.emoji) : ""; + const emoji = em ? [...em].slice(0, 2).join("") : "🎁"; if (!Array.isArray(raw.routes)) return null; const routes = []; for (const r of raw.routes.slice(0, 32)) { // hard cap