From 821d9aee3509aaf0facc9c1b203fc8579471efb6 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Tue, 31 Mar 2026 13:05:32 -0700 Subject: [PATCH] fix(share): sanitize public HTML and harden token handling Add a conservative sanitizer for shared note bodies before dangerouslySetInnerHTML. Normalize dynamic route params, reject blank tokens, and require noteId in the JSON payload so empty titles still render. Made-with: Cursor --- web/src/app/share/[token]/page.tsx | 16 +++++++++++++--- web/src/lib/sanitize-share-html.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 web/src/lib/sanitize-share-html.ts diff --git a/web/src/app/share/[token]/page.tsx b/web/src/app/share/[token]/page.tsx index f02d22d..fdd0e88 100644 --- a/web/src/app/share/[token]/page.tsx +++ b/web/src/app/share/[token]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { NOTES_API_URL, PRODUCT_NAME } from "@/lib/product-config"; +import { sanitizeSharedNoteHtml } from "@/lib/sanitize-share-html"; type PublicNote = { title: string; @@ -13,12 +14,17 @@ type PublicNote = { export default function SharedNotePage() { const params = useParams<{ token: string }>(); - const token = params.token; + const rawToken = params?.token; + const token = Array.isArray(rawToken) ? rawToken[0] : rawToken; const [note, setNote] = useState(null); const [error, setError] = useState(null); useEffect(() => { void (async () => { + if (!token?.trim()) { + setError("Invalid share link."); + return; + } try { const base = NOTES_API_URL.replace(/\/$/, ""); const res = await fetch(`${base}/public/note-shares/${encodeURIComponent(token)}`); @@ -26,7 +32,11 @@ export default function SharedNotePage() { setError("This share link is invalid or no longer available."); return; } - const data = (await res.json()) as PublicNote; + const data = (await res.json()) as PublicNote & { error?: string }; + if (!data.noteId) { + setError(data.error ?? "This share link is invalid or no longer available."); + return; + } setNote(data); } catch { setError("Could not load shared note."); @@ -52,7 +62,7 @@ export default function SharedNotePage() {
) : null} diff --git a/web/src/lib/sanitize-share-html.ts b/web/src/lib/sanitize-share-html.ts new file mode 100644 index 0000000..d93091b --- /dev/null +++ b/web/src/lib/sanitize-share-html.ts @@ -0,0 +1,12 @@ +/** + * Conservative sanitizer for untrusted note HTML on public share views. + * Strips common XSS vectors; not a full HTML parser. + */ +export function sanitizeSharedNoteHtml(html: string): string { + let s = html; + s = s.replace(/<(?:script|style|iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:script|style|iframe|object|embed)>/gi, ""); + s = s.replace(/<(?:script|style|iframe|object|embed)\b[^>]*\/?>/gi, ""); + s = s.replace(/\son\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, ""); + s = s.replace(/\shref\s*=\s*(?:"\s*javascript:[^"]*"|'\s*javascript:[^']*'|javascript:[^\s>]+)/gi, ' href="#"'); + return s; +}