Skip to content
File

Blob: src/client/lib/email-html/css.ts

typescript72 lines
1import { isAllowedInlineResourceUrl, isRemoteResourceUrl, isUnsafeUrl, stripQuotes } from "./url-policy";
2 
3export function sanitizeCssValue(value: string, allowRemoteContent: boolean) {
4 let blockedRemoteContent = false;
5 
6 const css = value
7 .replace(/@import[\s\S]*?;/gi, (match) => {
8 blockedRemoteContent = blockedRemoteContent || /https?:|\/\//i.test(match);
9 return "";
10 })
11 .replace(/expression\s*\([^)]*\)/gi, "")
12 .replace(/url\(([^)]+)\)/gi, (_match, rawUrl: string) => {
13 const nextUrl = stripQuotes(rawUrl);
14 
15 if (isUnsafeUrl(nextUrl)) {
16 return "none";
17 }
18 
19 if (isAllowedInlineResourceUrl(nextUrl)) {
20 return `url("${nextUrl}")`;
21 }
22 
23 if (allowRemoteContent && isRemoteResourceUrl(nextUrl)) {
24 return `url("${nextUrl}")`;
25 }
26 
27 if (isRemoteResourceUrl(nextUrl)) {
28 blockedRemoteContent = true;
29 }
30 
31 return "none";
32 })
33 .trim();
34 
35 return {
36 css,
37 blockedRemoteContent,
38 };
39}
40 
41export function sanitizeStyleSheet(value: string, allowRemoteContent: boolean) {
42 const sanitized = sanitizeCssValue(value, allowRemoteContent);
43 
44 return {
45 css: sanitized.css.replace(/behavior\s*:[^;]+;?/gi, "").trim(),
46 blockedRemoteContent: sanitized.blockedRemoteContent,
47 };
48}
49 
50export function sanitizeSrcSet(value: string, allowRemoteContent: boolean) {
51 const sources = value
52 .split(",")
53 .map((candidate) => candidate.trim())
54 .filter(Boolean)
55 .flatMap((candidate) => {
56 const [rawUrl, ...rest] = candidate.split(/\s+/);
57 const url = stripQuotes(rawUrl ?? "");
58 
59 if (!url || isUnsafeUrl(url)) {
60 return [] as string[];
61 }
62 
63 if (isAllowedInlineResourceUrl(url) || (allowRemoteContent && isRemoteResourceUrl(url))) {
64 return [rest.length > 0 ? `${url} ${rest.join(" ")}` : url];
65 }
66 
67 return [] as string[];
68 });
69 
70 return sources.join(", ");
71}