Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-30 - Fix XSS Vulnerability in Email Preview
**Vulnerability:** The email preview used `dangerouslySetInnerHTML` to render untrusted HTML, creating an XSS risk.
**Learning:** Even with DOMPurify, it is safer to use a sandboxed iframe to render untrusted HTML. Since iframes create an isolated browsing context, applying CSS classes (like Tailwind) directly to the `<iframe>` tag does not style the inner document; an inline `<style>` tag must be injected into the `srcDoc` HTML string to apply fonts, colors, or sizing to the content itself.
**Prevention:** Prioritize using a sandboxed `<iframe>` with the `srcDoc` attribute instead of `dangerouslySetInnerHTML` for dynamic or untrusted HTML. Include `sandbox="allow-popups allow-popups-to-escape-sandbox"` to permit link clicking while omitting `allow-scripts` and `allow-same-origin`.
32 changes: 29 additions & 3 deletions components/accounts/ppp-savings-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,28 @@ export function PppSavingsPanel({ orgSlug, accountId }: PppSavingsPanelProps) {
}
return `mailto:${encodeURIComponent(report.recipientEmail)}?subject=${encodeURIComponent(report.email.subject)}&body=${encodeURIComponent(draft)}`;
}, [draft, report]);
const sanitizedEmailHtml = useMemo(() => (report?.email.html ? DOMPurify.sanitize(report.email.html) : ""), [report]);
const sanitizedEmailHtml = useMemo(() => {
if (!report?.email.html) return "";
const cleanHtml = DOMPurify.sanitize(report.email.html);
return `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: sans-serif;
font-size: 0.875rem;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
${cleanHtml}
</body>
</html>
`;
}, [report]);

const pdfHref = report ? `/api/runtime/organizations/${orgSlug}/accounts/${accountId}/ppp-savings/pdf?year=${report.year}` : null;

Expand Down Expand Up @@ -232,8 +253,13 @@ export function PppSavingsPanel({ orgSlug, accountId }: PppSavingsPanelProps) {
)}
</div>
</div>
<div className="min-h-[24rem] w-full overflow-auto bg-white p-6 text-sm text-black">
<div dangerouslySetInnerHTML={{ __html: sanitizedEmailHtml }} />
<div className="min-h-[24rem] w-full bg-white p-6 text-sm text-black">
<iframe
title="Email Preview"
srcDoc={sanitizedEmailHtml}
sandbox="allow-popups allow-popups-to-escape-sandbox"
className="h-full min-h-[24rem] w-full border-none bg-transparent"
/>
</div>
</div>
</div>
Expand Down
Loading