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
This commit is contained in:
Saravana Achu Mac 2026-03-31 13:05:32 -07:00
parent 8d8540e320
commit 821d9aee35
2 changed files with 25 additions and 3 deletions

View File

@ -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<PublicNote | null>(null);
const [error, setError] = useState<string | null>(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() {
<div
className="input-shell"
style={{ marginTop: 16, minHeight: 200, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: note.body }}
dangerouslySetInnerHTML={{ __html: sanitizeSharedNoteHtml(note.body) }}
/>
</>
) : null}

View File

@ -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;
}