Improve email viewer content rendering
This commit is contained in:
Generated
+26
@@ -10,8 +10,10 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
@@ -2085,6 +2087,15 @@
|
||||
"@types/har-format": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz",
|
||||
@@ -2213,6 +2224,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webextension-polyfill": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.8.3.tgz",
|
||||
@@ -4877,6 +4894,15 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
|
||||
@@ -30,8 +30,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
|
||||
@@ -256,9 +256,10 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
<div className="bg-white mt-4">
|
||||
{email.messageHtml ? (
|
||||
<iframe
|
||||
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
|
||||
srcDoc={ConversionUtility.sanitizeAndPrepareEmailHtml(email.messageHtml)}
|
||||
className="w-full min-h-[500px] border-0"
|
||||
title={t('emails.emailContent')}
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-gray-800 p-3">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = (
|
||||
<WebView
|
||||
style={styles.webView}
|
||||
source={{ html: email.messageHtml }}
|
||||
source={{ html: sanitizedHtml }}
|
||||
scrollEnabled={true}
|
||||
javaScriptEnabled={false}
|
||||
onNavigationStateChange={(event) => {
|
||||
if (event.url !== 'about:blank') {
|
||||
// Open the URL in the browser
|
||||
|
||||
Generated
+72
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@EmailBody">
|
||||
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@EmailBody" sandbox="allow-popups allow-popups-to-escape-sandbox">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<!-- Email Content - Takes remaining space -->
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="text-gray-700 dark:text-gray-300 h-full">
|
||||
<iframe class="w-full h-full border-0" srcdoc="@EmailBody">
|
||||
<iframe class="w-full h-full border-0" srcdoc="@EmailBody" sandbox="allow-popups allow-popups-to-escape-sandbox">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -162,8 +162,9 @@ generate_html_body() {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>alert(origin)</script>
|
||||
<h1>$opening_text</h1>
|
||||
|
||||
|
||||
<div class="section">
|
||||
<p><strong>Random content:</strong> $content_suffix</p>
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.889" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace AliasVault.Shared.Utilities;
|
||||
|
||||
using Ganss.Xss;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,6 +15,156 @@ using HtmlAgilityPack;
|
||||
/// </summary>
|
||||
public static class ConversionUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly Lazy<HtmlSanitizer> 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;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes HTML content for safe display in email viewers.
|
||||
/// Removes all script tags, event handlers, and other XSS attack vectors.
|
||||
/// </summary>
|
||||
/// <param name="html">The HTML content to sanitize.</param>
|
||||
/// <returns>Sanitized HTML safe for display.</returns>
|
||||
/// <remarks>
|
||||
/// This method should be called before displaying any untrusted HTML content
|
||||
/// (e.g., received emails) to prevent Cross-Site Scripting (XSS) attacks.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="html">The HTML content to process.</param>
|
||||
/// <returns>Sanitized HTML with anchor tags configured to open in new tabs.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert all anchor tags to open in a new tab.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user