Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions packages/font/src/font.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type FontFormat =
type FontWeight = React.CSSProperties['fontWeight'];
type FontStyle = React.CSSProperties['fontStyle'];

function sanitizeCssValue(value: string): string {
return value.replace(/[';}{\\<>]/g, '');
}

export interface FontProps {
/** The font you want to use. NOTE: Do not insert multiple fonts here, use fallbackFontFamily for that */
fontFamily: string;
Expand All @@ -47,29 +51,28 @@ export const Font: React.FC<Readonly<FontProps>> = ({
fontStyle = 'normal',
fontWeight = 400,
}) => {
const safeFontFamily = sanitizeCssValue(fontFamily);
const safeFontStyle = sanitizeCssValue(String(fontStyle));
const safeFontWeight = sanitizeCssValue(String(fontWeight));
const safeFallbacks = Array.isArray(fallbackFontFamily)
? fallbackFontFamily.map(sanitizeCssValue)
: [sanitizeCssValue(fallbackFontFamily)];

const src = webFont
? `src: url(${webFont.url}) format('${webFont.format}');`
? `src: url(${sanitizeCssValue(webFont.url)}) format('${sanitizeCssValue(webFont.format)}');`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: sanitizeCssValue strips semicolons from webFont.url, which corrupts valid data URLs (e.g., data:font/woff;base64,...) and other URLs that rely on semicolons, breaking embedded fonts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/font/src/font.tsx, line 62:

<comment>sanitizeCssValue strips semicolons from webFont.url, which corrupts valid data URLs (e.g., data:font/woff;base64,...) and other URLs that rely on semicolons, breaking embedded fonts.</comment>

<file context>
@@ -47,29 +51,28 @@ export const Font: React.FC<Readonly<FontProps>> = ({
+
   const src = webFont
-    ? `src: url(${webFont.url}) format('${webFont.format}');`
+    ? `src: url(${sanitizeCssValue(webFont.url)}) format('${sanitizeCssValue(webFont.format)}');`
     : '';
 
</file context>

: '';

const style = `
@font-face {
font-family: '${fontFamily}';
font-style: ${fontStyle};
font-weight: ${fontWeight};
mso-font-alt: '${
Array.isArray(fallbackFontFamily)
? fallbackFontFamily[0]
: fallbackFontFamily
}';
font-family: '${safeFontFamily}';
font-style: ${safeFontStyle};
font-weight: ${safeFontWeight};
mso-font-alt: '${safeFallbacks[0]}';
${src}
}

* {
font-family: '${fontFamily}', ${
Array.isArray(fallbackFontFamily)
? fallbackFontFamily.join(', ')
: fallbackFontFamily
};
font-family: '${safeFontFamily}', ${safeFallbacks.join(', ')};
}
`;
return <style dangerouslySetInnerHTML={{ __html: style }} />;
Expand Down
2 changes: 1 addition & 1 deletion packages/markdown/src/markdown.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ console.log(\`Hello, $\{name}!\`);
</Markdown>,
);
expect(actualOutput).toMatchInlineSnapshot(`
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><div data-id="react-email-markdown"><p><strong style="font:700 23px / 32px &#x27;Roobert PRO&#x27;, system-ui, sans-serif;background:url(&#x27;path/to/image&#x27;)">This is sample bold text in markdown</strong> and <em style="font-style:italic">this is italic text</em></p>
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><div data-id="react-email-markdown"><p><strong style="font:700 23px / 32px &quot;Roobert PRO&quot;, system-ui, sans-serif;background:url(&quot;path/to/image&quot;)">This is sample bold text in markdown</strong> and <em style="font-style:italic">this is italic text</em></p>
</div><!--/$-->"
`);
});
Expand Down
19 changes: 14 additions & 5 deletions packages/markdown/src/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import * as React from 'react';
import { type StylesType, styles } from './styles';
import { parseCssInJsToInlineCss } from './utils/parse-css-in-js-to-inline-css';

function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

export type MarkdownProps = Readonly<{
children: string;
markdownCustomStyles?: StylesType;
Expand Down Expand Up @@ -37,7 +46,7 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(

// TODO: Support all options
renderer.code = ({ text }) => {
text = `${text.replace(/\n$/, '')}\n`;
text = `${escapeHtml(text).replace(/\n$/, '')}\n`;

return `<pre${
parseCssInJsToInlineCss(finalStyles.codeBlock) !== ''
Expand Down Expand Up @@ -97,8 +106,8 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
};

renderer.image = ({ href, text, title }) => {
return `<img src="${href.replaceAll('"', '&quot;')}" alt="${text.replaceAll('"', '&quot;')}"${
title ? ` title="${title}"` : ''
return `<img src="${escapeHtml(href)}" alt="${escapeHtml(text)}"${
title ? ` title="${escapeHtml(title)}"` : ''
}${
parseCssInJsToInlineCss(finalStyles.image) !== ''
? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"`
Expand All @@ -109,8 +118,8 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
renderer.link = ({ href, title, tokens }) => {
const text = renderer.parser.parseInline(tokens);

return `<a href="${href}" target="_blank"${
title ? ` title="${title}"` : ''
return `<a href="${escapeHtml(href)}" target="_blank"${
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Link hrefs are only HTML-escaped; dangerous schemes like javascript: remain valid and allow XSS. Escaping does not prevent executable URL protocols.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/markdown/src/markdown.tsx, line 121:

<comment>Link hrefs are only HTML-escaped; dangerous schemes like `javascript:` remain valid and allow XSS. Escaping does not prevent executable URL protocols.</comment>

<file context>
@@ -109,8 +118,8 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
 
-      return `<a href="${href}" target="_blank"${
-        title ? ` title="${title}"` : ''
+      return `<a href="${escapeHtml(href)}" target="_blank"${
+        title ? ` title="${escapeHtml(title)}"` : ''
       }${
</file context>

title ? ` title="${escapeHtml(title)}"` : ''
}${
parseCssInJsToInlineCss(finalStyles.link) !== ''
? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function camelToKebabCase(str: string): string {

function escapeQuotes(value: unknown) {
if (typeof value === 'string' && value.includes('"')) {
return value.replace(/"/g, '&#x27;');
return value.replace(/"/g, '&quot;');
}
return value;
}
Expand Down
Loading