security(webui): block remote mail preview content
This commit is contained in:
@@ -15,7 +15,7 @@ function assertDeepEqual(actual: unknown, expected: unknown, message = "values s
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import CredentialPanel from "../src/components/CredentialPanel";
|
||||
import PasswordField from "../src/components/PasswordField";
|
||||
import MessageDisplayPanel from "../src/components/MessageDisplayPanel";
|
||||
import MessageDisplayPanel, { buildSafeMessageHtmlDocument } from "../src/components/MessageDisplayPanel";
|
||||
import MailServerSettingsPanel, { MailServerFolderLookupResultView, resolveMailServerSettingsActiveSection } from "../src/components/mail/MailServerSettingsPanel";
|
||||
import EmailAddressInput from "../src/components/email/EmailAddressInput";
|
||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||
@@ -245,3 +245,44 @@ assert(messageDisplay.includes("agenda.pdf"), "direct attachment is rendered");
|
||||
assert(messageDisplay.includes("recipient-files.zip"), "ZIP archive attachment group is rendered");
|
||||
assert(messageDisplay.includes("i18n:govoplan-core.password_protected.09d9e174"), "ZIP protection badge is rendered");
|
||||
assert(messageDisplay.includes("zip_password"), "ZIP protection note is rendered");
|
||||
|
||||
const safeMessageDocument = buildSafeMessageHtmlDocument(`
|
||||
<base href="https://tracking.example/">
|
||||
<meta http-equiv="refresh" content="0;url=https://tracking.example/refresh">
|
||||
<link rel="stylesheet" href="https://tracking.example/mail.css">
|
||||
<style>
|
||||
@import "https://tracking.example/import.css";
|
||||
.remote { background-image: url(https://tracking.example/background.png); }
|
||||
.embedded { background-image: url('cid:background@example.org'); }
|
||||
</style>
|
||||
<a href="https://tracking.example/click" ping="https://tracking.example/ping">Remote link</a>
|
||||
<a href="#local-section">Local link</a>
|
||||
<img src="https://tracking.example/pixel.gif" srcset="https://tracking.example/retina.gif 2x" onerror="fetch('https://tracking.example/error')">
|
||||
<img src="cid:logo@example.org">
|
||||
<img src="data:image/png;base64,iVBORw0KGgo=">
|
||||
<img src="data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=">
|
||||
<script>fetch("https://tracking.example/script")</script>
|
||||
`);
|
||||
assert(safeMessageDocument.startsWith("<!doctype html><html><head>"), "message HTML is wrapped in an isolated document");
|
||||
assert(safeMessageDocument.includes("default-src 'none'"), "message iframe CSP denies resources by default");
|
||||
assert(safeMessageDocument.includes("img-src data: cid:"), "message iframe CSP permits only embedded image sources");
|
||||
assert(safeMessageDocument.includes("connect-src 'none'"), "message iframe CSP blocks active network connections");
|
||||
assert(safeMessageDocument.includes("form-action 'none'"), "message iframe CSP blocks form submissions");
|
||||
assert(safeMessageDocument.includes('src="cid:logo@example.org"'), "CID image references remain available");
|
||||
assert(safeMessageDocument.includes('src="data:image/png;base64,iVBORw0KGgo="'), "conservative raster data images remain available");
|
||||
assert(safeMessageDocument.includes('href="#local-section"'), "same-document fragment links remain available");
|
||||
assert(!safeMessageDocument.includes("tracking.example"), "remote message URLs are neutralized before preview");
|
||||
assert(!safeMessageDocument.includes("data:image/svg+xml"), "active SVG data resources are rejected");
|
||||
assert(!safeMessageDocument.includes("onerror="), "event-handler attributes are removed");
|
||||
assert(!safeMessageDocument.includes("<script"), "script elements are removed");
|
||||
|
||||
const privateMessageDisplay = renderToStaticMarkup(
|
||||
<MessageDisplayPanel
|
||||
title="Untrusted message"
|
||||
bodyHtml='<img src="https://tracking.example/pixel.gif"><img src="cid:logo@example.org">'
|
||||
preferredBodyMode="html"
|
||||
/>
|
||||
);
|
||||
assert(privateMessageDisplay.includes('sandbox=""'), "message HTML remains inside a scriptless iframe sandbox");
|
||||
assert(privateMessageDisplay.includes('referrerPolicy="no-referrer"'), "message iframe suppresses referrer data");
|
||||
assert(!privateMessageDisplay.includes("tracking.example"), "rendered iframe source contains no remote tracking URL");
|
||||
|
||||
Reference in New Issue
Block a user