feat(deps): isomorphic-dompurify for send-document preview hardening
Defense-in-depth XSS guard at the client-side preview boundary. `renderEmailBody()` already escapes-then-allowlists on the server, but mounting that output via dangerouslySetInnerHTML still exposes a single point of failure: a server-side regression in the sanitizer would silently produce a client-side XSS via the preview surface. DOMPurify sanitizes one more time before injection, with the exact allow-list `renderEmailBody` produces: <p>, <br>, <strong>, <em>, <code>, <a> (with href/target/rel, https/mailto only). Anything broader gets stripped at the DOM-injection boundary. Wrapped in useMemo so the sanitize only runs when the preview HTML changes — negligible perf, no per-render cost. The hand-rolled markdown-email.ts pipeline stays as-is: its escape-first-then-rule-replace architecture is correct and the "don't add DOMPurify as a dep at the conversion layer" reasoning in its header comment still holds. We add DOMPurify at the *consumer* boundary (preview rendering) where the threat model is "what if the server slips and emits unsafe HTML." Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -155,7 +156,19 @@ export function SendDocumentDialog({
|
||||
});
|
||||
|
||||
const unresolved = previewQuery.data?.data.unresolved ?? [];
|
||||
const previewHtml = previewQuery.data?.data.html ?? '';
|
||||
// Defense-in-depth: server-side `renderEmailBody()` already escapes
|
||||
// and allow-lists tags, but inject through DOMPurify before mounting
|
||||
// the preview into the DOM so a future server-side regression can't
|
||||
// silently produce a client-side XSS via the preview surface.
|
||||
const previewHtml = useMemo(
|
||||
() =>
|
||||
DOMPurify.sanitize(previewQuery.data?.data.html ?? '', {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'a'],
|
||||
ALLOWED_ATTR: ['href', 'target', 'rel'],
|
||||
ALLOWED_URI_REGEXP: /^https:|^mailto:/i,
|
||||
}),
|
||||
[previewQuery.data?.data.html],
|
||||
);
|
||||
|
||||
const recipientResolved = Boolean(recipientForApi.clientId || recipientForApi.email);
|
||||
const canPreview = recipientResolved;
|
||||
|
||||
Reference in New Issue
Block a user