diff --git a/apps/browser-extension/src/entrypoints/popup/utils/ConversionUtility.ts b/apps/browser-extension/src/entrypoints/popup/utils/ConversionUtility.ts
index ff19763f2..b7840cfe4 100644
--- a/apps/browser-extension/src/entrypoints/popup/utils/ConversionUtility.ts
+++ b/apps/browser-extension/src/entrypoints/popup/utils/ConversionUtility.ts
@@ -1,9 +1,85 @@
+import DOMPurify from 'dompurify';
+
+/**
+ * DOMPurify configuration for email viewing.
+ * Allows safe HTML elements for email display while blocking XSS vectors.
+ */
+const EMAIL_SANITIZER_CONFIG = {
+ ALLOWED_TAGS: [
+ 'div', 'span', 'p', 'br', 'hr',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'ul', 'ol', 'li',
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
+ 'a', 'img',
+ 'b', 'i', 'u', 's', 'strike', 'strong', 'em', 'small', 'sub', 'sup',
+ 'blockquote', 'pre', 'code',
+ 'font', 'center'
+ ],
+ ALLOWED_ATTR: [
+ 'style', 'class', 'id',
+ 'width', 'height', 'align', 'valign',
+ 'bgcolor', 'color', 'border',
+ 'cellpadding', 'cellspacing', 'colspan', 'rowspan',
+ 'face', 'size',
+ 'href', 'target', 'rel',
+ 'src', 'alt', 'title'
+ ],
+ ALLOW_DATA_ATTR: false,
+ ADD_ATTR: ['target'],
+ FORBID_TAGS: ['script', 'object', 'embed', 'iframe', 'frame', 'frameset',
+ 'form', 'input', 'button', 'textarea', 'select', 'option',
+ 'link', 'meta', 'base', 'applet'],
+ FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout',
+ 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onkeydown',
+ 'onkeyup', 'onkeypress', 'ondblclick', 'oncontextmenu', 'onmousedown',
+ 'onmouseup', 'onmousemove', 'ondrag', 'ondrop']
+};
+
/**
* Utility class for conversion operations.
* TODO: make this a shared utility class in root /core/ folder so we can reuse it between browser extension/mobile app
* and possibly WASM client.
*/
class ConversionUtility {
+
+ /**
+ * Sanitizes HTML content for safe display in email viewers.
+ * Removes all script tags, event handlers, and other XSS attack vectors.
+ * @param html The HTML content to sanitize.
+ * @returns Sanitized HTML safe for display.
+ */
+ public sanitizeHtmlForEmailViewing(html: string): string {
+ if (!html || html.trim() === '') {
+ return html;
+ }
+
+ try {
+ return DOMPurify.sanitize(html, EMAIL_SANITIZER_CONFIG);
+ } catch (ex) {
+ console.error(`Error in sanitizeHtmlForEmailViewing: ${ex instanceof Error ? ex.message : String(ex)}`);
+ // Return empty string on error to prevent potential XSS
+ return '';
+ }
+ }
+
+ /**
+ * Sanitizes HTML content and converts anchor tags to open in a new tab.
+ * This is a convenience method that combines sanitization with anchor tag conversion.
+ * @param html The HTML content to process.
+ * @returns Sanitized HTML with anchor tags configured to open in new tabs.
+ */
+ public sanitizeAndPrepareEmailHtml(html: string): string {
+ if (!html || html.trim() === '') {
+ return html;
+ }
+
+ // First sanitize to remove XSS vectors
+ const sanitizedHtml = this.sanitizeHtmlForEmailViewing(html);
+
+ // Then convert anchor tags to open in new tab
+ return this.convertAnchorTagsToOpenInNewTab(sanitizedHtml);
+ }
+
/**
* Convert all anchor tags to open in a new tab.
* @param html HTML input.
diff --git a/apps/mobile-app/app/(tabs)/emails/[id].tsx b/apps/mobile-app/app/(tabs)/emails/[id].tsx
index fe30a167b..c4d4804f8 100644
--- a/apps/mobile-app/app/(tabs)/emails/[id].tsx
+++ b/apps/mobile-app/app/(tabs)/emails/[id].tsx
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
import { StyleSheet, View, ActivityIndicator, Share, useColorScheme, Linking, Text, TextInput, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
+import ConversionUtility from '@/utils/ConversionUtility';
import type { Item } from '@/utils/dist/core/models/vault';
import type { Email } from '@/utils/dist/core/models/webapi';
import EncryptionUtility from '@/utils/EncryptionUtility';
@@ -481,11 +482,14 @@ export default function EmailDetailsScreen() : React.ReactNode {
let emailView = null;
if (isHtmlView && email.messageHtml) {
+ // Sanitize HTML
+ const sanitizedHtml = ConversionUtility.sanitizeHtmlForEmailViewing(email.messageHtml);
emailView = (
{
if (event.url !== 'about:blank') {
// Open the URL in the browser
diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json
index b5d18fb49..91b10a3e4 100644
--- a/apps/mobile-app/package-lock.json
+++ b/apps/mobile-app/package-lock.json
@@ -55,6 +55,7 @@
"react-native-svg-transformer": "^1.5.0",
"react-native-toast-message": "^2.2.1",
"react-native-webview": "13.13.5",
+ "sanitize-html": "^2.17.0",
"yup": "^1.6.1"
},
"devDependencies": {
@@ -68,6 +69,7 @@
"@types/lodash": "^4.17.16",
"@types/react": "~19.0.10",
"@types/react-test-renderer": "^18.3.0",
+ "@types/sanitize-html": "^2.16.0",
"@types/sql.js": "^1.4.9",
"@types/yup": "^0.29.14",
"eslint": "^9.35.0",
@@ -4777,6 +4779,16 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/sanitize-html": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz",
+ "integrity": "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "htmlparser2": "^8.0.0"
+ }
+ },
"node_modules/@types/sql.js": {
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.9.tgz",
@@ -9872,6 +9884,37 @@
"void-elements": "3.1.0"
}
},
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -10475,6 +10518,15 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -13810,6 +13862,12 @@
"node": ">=10"
}
},
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
+ "license": "MIT"
+ },
"node_modules/parse-statements": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz",
@@ -15370,6 +15428,20 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/sanitize-html": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
+ "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.2.2",
+ "escape-string-regexp": "^4.0.0",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ }
+ },
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json
index 90e670aae..65e6b2e51 100644
--- a/apps/mobile-app/package.json
+++ b/apps/mobile-app/package.json
@@ -76,6 +76,7 @@
"react-native-svg-transformer": "^1.5.0",
"react-native-toast-message": "^2.2.1",
"react-native-webview": "13.13.5",
+ "sanitize-html": "^2.17.0",
"yup": "^1.6.1"
},
"devDependencies": {
@@ -89,6 +90,7 @@
"@types/lodash": "^4.17.16",
"@types/react": "~19.0.10",
"@types/react-test-renderer": "^18.3.0",
+ "@types/sanitize-html": "^2.16.0",
"@types/sql.js": "^1.4.9",
"@types/yup": "^0.29.14",
"eslint": "^9.35.0",
diff --git a/apps/mobile-app/utils/ConversionUtility.ts b/apps/mobile-app/utils/ConversionUtility.ts
index 161a55fd1..a9c18d39f 100644
--- a/apps/mobile-app/utils/ConversionUtility.ts
+++ b/apps/mobile-app/utils/ConversionUtility.ts
@@ -1,9 +1,70 @@
+import sanitizeHtml from 'sanitize-html';
+
+/**
+ * sanitize-html configuration for email viewing.
+ * Allows safe HTML elements for email display while blocking XSS vectors.
+ * Note: Using sanitize-html instead of DOMPurify because React Native
+ * doesn't have a DOM environment that DOMPurify requires.
+ */
+const EMAIL_SANITIZER_CONFIG: sanitizeHtml.IOptions = {
+ // Disable style parsing as it requires PostCSS which doesn't work in React Native
+ // See: https://github.com/apostrophecms/sanitize-html/issues/547
+ parseStyleAttributes: false,
+ allowedTags: [
+ 'div', 'span', 'p', 'br', 'hr',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'ul', 'ol', 'li',
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
+ 'a', 'img',
+ 'b', 'i', 'u', 's', 'strike', 'strong', 'em', 'small', 'sub', 'sup',
+ 'blockquote', 'pre', 'code',
+ 'font', 'center'
+ ],
+ allowedAttributes: {
+ '*': ['style', 'class', 'id'],
+ 'table': ['width', 'height', 'align', 'valign', 'bgcolor', 'border', 'cellpadding', 'cellspacing'],
+ 'tr': ['align', 'valign', 'bgcolor'],
+ 'th': ['width', 'height', 'align', 'valign', 'bgcolor', 'colspan', 'rowspan'],
+ 'td': ['width', 'height', 'align', 'valign', 'bgcolor', 'colspan', 'rowspan'],
+ 'a': ['href', 'target', 'rel'],
+ 'img': ['src', 'alt', 'title', 'width', 'height'],
+ 'font': ['color', 'face', 'size']
+ },
+ allowedSchemes: ['http', 'https', 'mailto'],
+ // Disable data: URIs for security (can be used for XSS)
+ allowedSchemesByTag: {
+ img: ['http', 'https'],
+ a: ['http', 'https', 'mailto']
+ }
+};
+
/**
* Utility class for conversion operations.
* TODO: make this a shared utility class in root /core/ folder so we can reuse it between
* browser extension/mobile app and possibly WASM client.
*/
class ConversionUtility {
+
+ /**
+ * Sanitizes HTML content for safe display in email viewers.
+ * Removes all script tags, event handlers, and other XSS attack vectors.
+ * @param html The HTML content to sanitize.
+ * @returns Sanitized HTML safe for display.
+ */
+ public sanitizeHtmlForEmailViewing(html: string): string {
+ if (!html || html.trim() === '') {
+ return html;
+ }
+
+ try {
+ return sanitizeHtml(html, EMAIL_SANITIZER_CONFIG);
+ } catch (ex) {
+ console.error(`Error in sanitizeHtmlForEmailViewing: ${ex instanceof Error ? ex.message : String(ex)}`);
+ // Return empty string on error to prevent potential XSS
+ return '';
+ }
+ }
+
/**
* Normalize a username by converting it to lowercase and trimming whitespace.
* @param username The username to normalize.
diff --git a/apps/server/AliasVault.Client/Main/Components/Email/EmailModal.razor b/apps/server/AliasVault.Client/Main/Components/Email/EmailModal.razor
index 55cad34be..8f70723b1 100644
--- a/apps/server/AliasVault.Client/Main/Components/Email/EmailModal.razor
+++ b/apps/server/AliasVault.Client/Main/Components/Email/EmailModal.razor
@@ -51,7 +51,7 @@
-
@@ -248,8 +248,8 @@
// Check if there is HTML content, if not, then set default viewtype to plain
if (Email.MessageHtml is not null && !string.IsNullOrWhiteSpace(Email.MessageHtml))
{
- // HTML is available
- EmailBody = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(Email.MessageHtml);
+ // HTML is available, sanitize and prepare for display
+ EmailBody = ConversionUtility.SanitizeAndPrepareEmailHtml(Email.MessageHtml);
}
else if (Email.MessagePlain is not null)
{
diff --git a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor
index 06466a4fa..ff50af047 100644
--- a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor
+++ b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor
@@ -64,7 +64,7 @@
-
+
@@ -269,8 +269,8 @@
// Check if there is HTML content, if not, then set default viewtype to plain
if (Email.MessageHtml is not null && !string.IsNullOrWhiteSpace(Email.MessageHtml))
{
- // HTML is available
- EmailBody = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(Email.MessageHtml);
+ // HTML is available, sanitize and prepare for display
+ EmailBody = ConversionUtility.SanitizeAndPrepareEmailHtml(Email.MessageHtml);
}
else if (Email.MessagePlain is not null)
{
diff --git a/apps/server/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh b/apps/server/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh
index b13875e8a..16f1c18df 100755
--- a/apps/server/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh
+++ b/apps/server/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh
@@ -162,8 +162,9 @@ generate_html_body() {
+
$opening_text
-
+
Random content: $content_suffix
@@ -203,12 +204,12 @@ generate_headers() {
local subject="$2"
local content_type="$3"
local boundary="$4"
-
+
printf "From: sender@example.com\r\n"
printf "To: %s\r\n" "$recipient"
printf "Subject: %s\r\n" "$subject"
printf "MIME-Version: 1.0\r\n"
-
+
if [[ -n "$boundary" ]]; then
printf "Content-Type: multipart/mixed; boundary=%s\r\n" "$boundary"
else
@@ -229,7 +230,7 @@ send_email() {
local email_type="$2"
local smtp_port="$3"
local email_number="$4"
-
+
# Generate common random elements
local subject_suffix=$(generate_random_string 8)
local content_suffix=$(generate_random_content)
@@ -238,46 +239,46 @@ send_email() {
local random_unicode="Unicode test: δ½ ε₯½δΈη π ζ΅θ―ζε π"
local subject_unicode=$(generate_random_unicode_subject 6)
local chinese_text=$(generate_random_chinese 8)
-
+
# Determine email properties based on type
local with_attachment="false"
local is_html="false"
local content_type="text/plain"
-
+
case "$email_type" in
2) with_attachment="true" ;;
3) is_html="true"; content_type="text/html" ;;
4) with_attachment="true"; is_html="true"; content_type="text/html" ;;
esac
-
+
# Build subject line
local subject="Test Email #$email_number"
[[ "$with_attachment" == "true" ]] && subject="$subject with Attachment"
subject="$subject $subject_unicode - $subject_suffix"
-
+
# Handle emails with attachments
if [[ "$with_attachment" == "true" ]]; then
local boundary="boundary-$(generate_random_string 16)"
local attachment_content="This is a test attachment content - $(generate_random_string 32)"
local attachment_name="test_attachment_$(generate_random_string 8).txt"
-
+
{
generate_headers "$recipient" "$subject" "" "$boundary"
-
+
# Email body part
printf -- "--%s\r\n" "$boundary"
printf "Content-Type: %s; charset=utf-8\r\n" "$content_type"
printf "Content-Transfer-Encoding: 8bit\r\n"
printf "\r\n"
-
+
if [[ "$is_html" == "true" ]]; then
generate_html_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
else
generate_plain_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
fi
-
+
printf "\r\n"
-
+
# Attachment part
printf -- "--%s\r\n" "$boundary"
printf "Content-Type: application/octet-stream\r\n"
@@ -295,7 +296,7 @@ send_email() {
# Handle emails without attachments
{
generate_headers "$recipient" "$subject" "$content_type" ""
-
+
if [[ "$is_html" == "true" ]]; then
generate_html_body "$email_number" "$content_suffix" "$chinese_text" "$special_chars" "$emoji_text" "$random_unicode" "$with_attachment"
else
@@ -331,7 +332,7 @@ select_email_type() {
echo "3) HTML" >&2
echo "4) HTML with attachment" >&2
echo "" >&2
-
+
local email_type
while true; do
read -p "Enter your choice (1-4): " email_type
@@ -348,7 +349,7 @@ while true; do
if [[ -z "$recipient" ]]; then
read -p "Enter the recipient's email address: " recipient
fi
-
+
if [[ -z "$email_type" ]]; then
email_type=$(select_email_type)
fi
diff --git a/apps/server/Shared/AliasVault.Shared/AliasVault.Shared.csproj b/apps/server/Shared/AliasVault.Shared/AliasVault.Shared.csproj
index 6ce68f970..fc60c2c22 100644
--- a/apps/server/Shared/AliasVault.Shared/AliasVault.Shared.csproj
+++ b/apps/server/Shared/AliasVault.Shared/AliasVault.Shared.csproj
@@ -23,6 +23,7 @@
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/apps/server/Shared/AliasVault.Shared/Utilities/ConversionUtility.cs b/apps/server/Shared/AliasVault.Shared/Utilities/ConversionUtility.cs
index 09cce0686..5a9a5ef40 100644
--- a/apps/server/Shared/AliasVault.Shared/Utilities/ConversionUtility.cs
+++ b/apps/server/Shared/AliasVault.Shared/Utilities/ConversionUtility.cs
@@ -7,6 +7,7 @@
namespace AliasVault.Shared.Utilities;
+using Ganss.Xss;
using HtmlAgilityPack;
///
@@ -14,6 +15,156 @@ using HtmlAgilityPack;
///
public static class ConversionUtility
{
+ ///
+ /// Lazy-initialized HTML sanitizer instance configured for safe email viewing.
+ /// Removes all script tags, event handlers, and other XSS vectors while preserving
+ /// safe HTML for email display.
+ ///
+ private static readonly Lazy EmailSanitizer = new(() =>
+ {
+ var sanitizer = new HtmlSanitizer();
+
+ // Allow common email formatting elements
+ sanitizer.AllowedTags.Add("div");
+ sanitizer.AllowedTags.Add("span");
+ sanitizer.AllowedTags.Add("p");
+ sanitizer.AllowedTags.Add("br");
+ sanitizer.AllowedTags.Add("hr");
+ sanitizer.AllowedTags.Add("h1");
+ sanitizer.AllowedTags.Add("h2");
+ sanitizer.AllowedTags.Add("h3");
+ sanitizer.AllowedTags.Add("h4");
+ sanitizer.AllowedTags.Add("h5");
+ sanitizer.AllowedTags.Add("h6");
+ sanitizer.AllowedTags.Add("ul");
+ sanitizer.AllowedTags.Add("ol");
+ sanitizer.AllowedTags.Add("li");
+ sanitizer.AllowedTags.Add("table");
+ sanitizer.AllowedTags.Add("thead");
+ sanitizer.AllowedTags.Add("tbody");
+ sanitizer.AllowedTags.Add("tfoot");
+ sanitizer.AllowedTags.Add("tr");
+ sanitizer.AllowedTags.Add("th");
+ sanitizer.AllowedTags.Add("td");
+ sanitizer.AllowedTags.Add("a");
+ sanitizer.AllowedTags.Add("img");
+ sanitizer.AllowedTags.Add("b");
+ sanitizer.AllowedTags.Add("i");
+ sanitizer.AllowedTags.Add("u");
+ sanitizer.AllowedTags.Add("s");
+ sanitizer.AllowedTags.Add("strike");
+ sanitizer.AllowedTags.Add("strong");
+ sanitizer.AllowedTags.Add("em");
+ sanitizer.AllowedTags.Add("small");
+ sanitizer.AllowedTags.Add("sub");
+ sanitizer.AllowedTags.Add("sup");
+ sanitizer.AllowedTags.Add("blockquote");
+ sanitizer.AllowedTags.Add("pre");
+ sanitizer.AllowedTags.Add("code");
+ sanitizer.AllowedTags.Add("font");
+ sanitizer.AllowedTags.Add("center");
+
+ // Allow common styling attributes
+ sanitizer.AllowedAttributes.Add("style");
+ sanitizer.AllowedAttributes.Add("class");
+ sanitizer.AllowedAttributes.Add("id");
+ sanitizer.AllowedAttributes.Add("width");
+ sanitizer.AllowedAttributes.Add("height");
+ sanitizer.AllowedAttributes.Add("align");
+ sanitizer.AllowedAttributes.Add("valign");
+ sanitizer.AllowedAttributes.Add("bgcolor");
+ sanitizer.AllowedAttributes.Add("color");
+ sanitizer.AllowedAttributes.Add("border");
+ sanitizer.AllowedAttributes.Add("cellpadding");
+ sanitizer.AllowedAttributes.Add("cellspacing");
+ sanitizer.AllowedAttributes.Add("colspan");
+ sanitizer.AllowedAttributes.Add("rowspan");
+ sanitizer.AllowedAttributes.Add("face");
+ sanitizer.AllowedAttributes.Add("size");
+
+ // Allow href for links but sanitize URLs
+ sanitizer.AllowedAttributes.Add("href");
+ sanitizer.AllowedAttributes.Add("target");
+ sanitizer.AllowedAttributes.Add("rel");
+
+ // Allow src for images but sanitize URLs
+ sanitizer.AllowedAttributes.Add("src");
+ sanitizer.AllowedAttributes.Add("alt");
+ sanitizer.AllowedAttributes.Add("title");
+
+ // Explicitly remove dangerous elements (fallback)
+ sanitizer.AllowedTags.Remove("script");
+ sanitizer.AllowedTags.Remove("object");
+ sanitizer.AllowedTags.Remove("embed");
+ sanitizer.AllowedTags.Remove("iframe");
+ sanitizer.AllowedTags.Remove("frame");
+ sanitizer.AllowedTags.Remove("frameset");
+ sanitizer.AllowedTags.Remove("form");
+ sanitizer.AllowedTags.Remove("input");
+ sanitizer.AllowedTags.Remove("button");
+ sanitizer.AllowedTags.Remove("textarea");
+ sanitizer.AllowedTags.Remove("select");
+ sanitizer.AllowedTags.Remove("option");
+ sanitizer.AllowedTags.Remove("link");
+ sanitizer.AllowedTags.Remove("meta");
+ sanitizer.AllowedTags.Remove("base");
+ sanitizer.AllowedTags.Remove("applet");
+
+ return sanitizer;
+ });
+
+ ///
+ /// Sanitizes HTML content for safe display in email viewers.
+ /// Removes all script tags, event handlers, and other XSS attack vectors.
+ ///
+ /// The HTML content to sanitize.
+ /// Sanitized HTML safe for display.
+ ///
+ /// This method should be called before displaying any untrusted HTML content
+ /// (e.g., received emails) to prevent Cross-Site Scripting (XSS) attacks.
+ ///
+ public static string SanitizeHtmlForEmailViewing(string html)
+ {
+ if (string.IsNullOrWhiteSpace(html))
+ {
+ return html;
+ }
+
+ try
+ {
+ return EmailSanitizer.Value.Sanitize(html);
+ }
+ catch (Exception ex)
+ {
+ // Log the exception
+ Console.WriteLine($"Error in SanitizeHtmlForEmailViewing: {ex.Message}");
+
+ // Return empty string on error to prevent potential XSS
+ // This is safer than returning the original HTML
+ return string.Empty;
+ }
+ }
+
+ ///
+ /// Sanitizes HTML content and converts anchor tags to open in a new tab.
+ /// This is a convenience method that combines sanitization with anchor tag conversion.
+ ///
+ /// The HTML content to process.
+ /// Sanitized HTML with anchor tags configured to open in new tabs.
+ public static string SanitizeAndPrepareEmailHtml(string html)
+ {
+ if (string.IsNullOrWhiteSpace(html))
+ {
+ return html;
+ }
+
+ // First sanitize to remove XSS vectors
+ var sanitizedHtml = SanitizeHtmlForEmailViewing(html);
+
+ // Then convert anchor tags to open in new tab
+ return ConvertAnchorTagsToOpenInNewTab(sanitizedHtml);
+ }
+
///
/// Convert all anchor tags to open in a new tab.
///