mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-07 08:31:01 +01:00
feat: enhance sanitization
This commit is contained in:
+9
-23
@@ -4,6 +4,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { createIcons } from 'lucide';
|
||||
import { state, resetState } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
|
||||
const STANDARD_SIZES = {
|
||||
@@ -319,19 +320,12 @@ export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
export function sanitizeEmailHtml(html: string): string {
|
||||
if (!html) return html;
|
||||
|
||||
let sanitized = html;
|
||||
let sanitized = DOMPurify.sanitize(html, {
|
||||
FORBID_TAGS: ['style', 'link', 'script', 'iframe', 'object', 'embed'],
|
||||
FORBID_ATTR: ['style'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
});
|
||||
|
||||
sanitized = sanitized.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '');
|
||||
sanitized = sanitized.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||||
sanitized = sanitized.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
sanitized = sanitized.replace(/<link[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/\s+style=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(/\s+class=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(/\s+data-[a-z-]+=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(
|
||||
/<img[^>]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi,
|
||||
''
|
||||
);
|
||||
sanitized = sanitized.replace(
|
||||
/href=["']https?:\/\/[^"']*safelinks\.protection\.outlook\.com[^"']*url=([^&"']+)[^"']*["']/gi,
|
||||
(match, encodedUrl) => {
|
||||
@@ -343,10 +337,9 @@ export function sanitizeEmailHtml(html: string): string {
|
||||
}
|
||||
}
|
||||
);
|
||||
sanitized = sanitized.replace(/\s+originalsrc=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(
|
||||
/href=["']([^"']{500,})["']/gi,
|
||||
(match, url) => {
|
||||
(_match, url: string) => {
|
||||
const baseUrl = url.split('?')[0];
|
||||
if (baseUrl && baseUrl.length < 200) {
|
||||
return `href="${baseUrl}"`;
|
||||
@@ -354,15 +347,12 @@ export function sanitizeEmailHtml(html: string): string {
|
||||
return `href="${url.substring(0, 200)}"`;
|
||||
}
|
||||
);
|
||||
|
||||
sanitized = sanitized.replace(
|
||||
/\s+(cellpadding|cellspacing|bgcolor|border|valign|align|width|height|role|dir|id)=["'][^"']*["']/gi,
|
||||
/<img[^>]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi,
|
||||
''
|
||||
);
|
||||
sanitized = sanitized.replace(/<\/?table[^>]*>/gi, '<div>');
|
||||
sanitized = sanitized.replace(/<\/?tbody[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<\/?thead[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<\/?tfoot[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<\/?(tbody|thead|tfoot)[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<tr[^>]*>/gi, '<div>');
|
||||
sanitized = sanitized.replace(/<\/tr>/gi, '</div>');
|
||||
sanitized = sanitized.replace(/<td[^>]*>/gi, '<span> ');
|
||||
@@ -373,10 +363,6 @@ export function sanitizeEmailHtml(html: string): string {
|
||||
sanitized = sanitized.replace(/<span>\s*<\/span>/gi, '');
|
||||
sanitized = sanitized.replace(/(<div>)+/gi, '<div>');
|
||||
sanitized = sanitized.replace(/(<\/div>)+/gi, '</div>');
|
||||
sanitized = sanitized.replace(
|
||||
/<a[^>]*href=["']\s*["'][^>]*>([^<]*)<\/a>/gi,
|
||||
'$1'
|
||||
);
|
||||
|
||||
const MAX_HTML_SIZE = 100000;
|
||||
if (sanitized.length > MAX_HTML_SIZE) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import DOMPurify from 'dompurify';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
@@ -297,7 +298,7 @@ export class MarkdownEditor {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose',
|
||||
securityLevel: 'strict',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
});
|
||||
@@ -691,7 +692,9 @@ export class MarkdownEditor {
|
||||
|
||||
const markdown = this.editor.value;
|
||||
const html = this.md.render(markdown);
|
||||
this.preview.innerHTML = html;
|
||||
this.preview.innerHTML = DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: ['target'],
|
||||
});
|
||||
this.renderMermaidDiagrams();
|
||||
}
|
||||
|
||||
@@ -714,7 +717,9 @@ export class MarkdownEditor {
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mermaid-diagram';
|
||||
wrapper.innerHTML = svg;
|
||||
wrapper.innerHTML = DOMPurify.sanitize(svg, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true },
|
||||
});
|
||||
|
||||
pre.replaceWith(wrapper);
|
||||
} catch (error) {
|
||||
@@ -740,7 +745,9 @@ export class MarkdownEditor {
|
||||
}
|
||||
|
||||
public getHtml(): string {
|
||||
return this.md.render(this.getContent());
|
||||
return DOMPurify.sanitize(this.md.render(this.getContent()), {
|
||||
ADD_ATTR: ['target'],
|
||||
});
|
||||
}
|
||||
|
||||
private exportPdf(): void {
|
||||
|
||||
@@ -10,8 +10,8 @@ const STORAGE_KEY = 'bentopdf:wasm-providers';
|
||||
|
||||
const CDN_DEFAULTS: Record<WasmPackage, string> = {
|
||||
pymupdf: 'https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/',
|
||||
ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/',
|
||||
cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf/dist/',
|
||||
ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/',
|
||||
cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/',
|
||||
};
|
||||
|
||||
function envOrDefault(envVar: string | undefined, fallback: string): string {
|
||||
@@ -30,20 +30,77 @@ const ENV_DEFAULTS: Record<WasmPackage, string> = {
|
||||
cpdf: envOrDefault(import.meta.env.VITE_WASM_CPDF_URL, CDN_DEFAULTS.cpdf),
|
||||
};
|
||||
|
||||
function hostnameOf(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectBuiltinTrustedHosts(): Set<string> {
|
||||
const hosts = new Set<string>();
|
||||
if (typeof location !== 'undefined' && location.hostname) {
|
||||
hosts.add(location.hostname);
|
||||
}
|
||||
for (const url of Object.values(CDN_DEFAULTS)) {
|
||||
const h = hostnameOf(url);
|
||||
if (h) hosts.add(h);
|
||||
}
|
||||
for (const url of Object.values(ENV_DEFAULTS)) {
|
||||
const h = hostnameOf(url);
|
||||
if (h) hosts.add(h);
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
const BUILTIN_TRUSTED_HOSTS = collectBuiltinTrustedHosts();
|
||||
|
||||
class WasmProviderManager {
|
||||
private config: WasmProviderConfig;
|
||||
private validationCache: Map<WasmPackage, boolean> = new Map();
|
||||
private trustedHosts: Set<string> = new Set<string>(BUILTIN_TRUSTED_HOSTS);
|
||||
|
||||
constructor() {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
private isTrustedUrl(url: string): boolean {
|
||||
const host = hostnameOf(url);
|
||||
return !!host && this.trustedHosts.has(host);
|
||||
}
|
||||
|
||||
private loadConfig(): WasmProviderConfig {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
if (!stored) return {};
|
||||
const parsed = JSON.parse(stored) as WasmProviderConfig;
|
||||
const safe: WasmProviderConfig = {};
|
||||
let dropped = false;
|
||||
for (const key of ['pymupdf', 'ghostscript', 'cpdf'] as WasmPackage[]) {
|
||||
const url = parsed[key];
|
||||
if (typeof url !== 'string') continue;
|
||||
if (this.isTrustedUrl(url)) {
|
||||
safe[key] = url;
|
||||
} else {
|
||||
dropped = true;
|
||||
console.warn(
|
||||
`[WasmProvider] Ignoring untrusted stored URL for ${key}: ${url}. ` +
|
||||
'Reconfigure via Advanced Settings to re-enable.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (dropped) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(safe));
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'[WasmProvider] Failed to scrub untrusted config from localStorage:',
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[WasmProvider] Failed to load config from localStorage:',
|
||||
@@ -66,11 +123,23 @@ class WasmProviderManager {
|
||||
}
|
||||
|
||||
getUrl(packageName: WasmPackage): string | undefined {
|
||||
return this.config[packageName] || this.getEnvDefault(packageName);
|
||||
const stored = this.config[packageName];
|
||||
if (stored) {
|
||||
if (this.isTrustedUrl(stored)) return stored;
|
||||
console.warn(
|
||||
`[WasmProvider] Refusing to use untrusted URL for ${packageName}; falling back to env default.`
|
||||
);
|
||||
}
|
||||
return this.getEnvDefault(packageName);
|
||||
}
|
||||
|
||||
setUrl(packageName: WasmPackage, url: string): void {
|
||||
const normalizedUrl = url.endsWith('/') ? url : `${url}/`;
|
||||
const host = hostnameOf(normalizedUrl);
|
||||
if (!host) {
|
||||
throw new Error('Invalid URL');
|
||||
}
|
||||
this.trustedHosts.add(host);
|
||||
this.config[packageName] = normalizedUrl;
|
||||
this.validationCache.delete(packageName);
|
||||
this.saveConfig();
|
||||
@@ -219,6 +288,7 @@ class WasmProviderManager {
|
||||
clearAll(): void {
|
||||
this.config = {};
|
||||
this.validationCache.clear();
|
||||
this.trustedHosts = new Set<string>(BUILTIN_TRUSTED_HOSTS);
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user