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
9 changes: 9 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 2024-07-01 - Prevent reverse tabnabbing on external links
**Vulnerability:** External links with `target="_blank"` missing `rel="noopener noreferrer"`.
**Learning:** React/Next.js might not always auto-add these attributes depending on how the tag is structured, and it's safer to be explicit to prevent reverse tabnabbing.
**Prevention:** Always explicitly include `rel="noopener noreferrer"` when using `target="_blank"`.

## 2024-07-01 - Prevent XSS in dynamic email rendering
**Vulnerability:** Using `dangerouslySetInnerHTML` directly in the DOM to render dynamic email HTML (even if sanitized by DOMPurify).
**Learning:** DOMPurify sanitizes, but doesn't isolate the environment. An attacker could potentially find a DOMPurify bypass, or CSS could leak and affect the application UI. Rendering untrusted HTML (like an email preview) should be done in an isolated environment.
**Prevention:** Always use a sandboxed `<iframe>` with `srcDoc` and `sandbox="allow-popups allow-popups-to-escape-sandbox"` instead of `dangerouslySetInnerHTML` when rendering dynamic, potentially untrusted HTML content like emails.
10 changes: 7 additions & 3 deletions components/accounts/ppp-savings-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ 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 `<style>body { font-family: sans-serif; color: black; margin: 0; padding: 24px; }</style>${cleanHtml}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep preview CSS out of copied emails

When the browser supports ClipboardItem, copyDraft() writes this same sanitizedEmailHtml as the clipboard's text/html. The new <style>body { ... margin: 0; padding: 24px; }</style> is only needed to pad the sandboxed preview, but it will be pasted into the outbound email too, so copied PPP drafts can get an extra body reset/padding or have the style tag stripped unpredictably by the mail client. Split preview HTML from clipboard HTML so copying preserves the generated email markup.

Useful? React with πŸ‘Β / πŸ‘Ž.

}, [report]);

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

Expand Down Expand Up @@ -232,8 +236,8 @@ 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 text-sm text-black">
<iframe srcDoc={sanitizedEmailHtml} sandbox="allow-popups allow-popups-to-escape-sandbox" className="h-full min-h-[24rem] w-full border-none" title="Email preview" />
</div>
</div>
</div>
Expand Down
Loading