From 0aef517dc01926a456b800f6379a7375e4a0e516 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Tue, 23 Jun 2026 13:06:02 +0530 Subject: [PATCH] fix(markdown): escape double quotes in link href/title and image title Image src/alt were already escaped, but link href, link title, and image title were interpolated raw. A title like 'The "Complete" Guide' or an href with a quoted query value broke out of the attribute in the rendered HTML. Escape them the same way. --- .changeset/markdown-escape-link-quotes.md | 5 +++++ .../src/components/markdown/markdown.spec.tsx | 12 ++++++++++++ .../react-email/src/components/markdown/markdown.tsx | 6 +++--- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 .changeset/markdown-escape-link-quotes.md diff --git a/.changeset/markdown-escape-link-quotes.md b/.changeset/markdown-escape-link-quotes.md new file mode 100644 index 0000000000..bbca79e940 --- /dev/null +++ b/.changeset/markdown-escape-link-quotes.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +Escape double quotes in `Markdown` link `href`/`title` and image `title` attributes, matching the escaping already applied to image `src`/`alt`. A markdown title like `'The "Complete" Guide'` no longer breaks out of the attribute in the rendered HTML. diff --git a/packages/react-email/src/components/markdown/markdown.spec.tsx b/packages/react-email/src/components/markdown/markdown.spec.tsx index c3f08f9330..74e388ed24 100644 --- a/packages/react-email/src/components/markdown/markdown.spec.tsx +++ b/packages/react-email/src/components/markdown/markdown.spec.tsx @@ -131,6 +131,18 @@ console.log(\`Hello, $\{name}!\`); `); }); + it('escapes double quotes in link/image href and title attributes', async () => { + const actualOutput = await render( + + {`[guide](https://example.com/?q="a" 'The "Complete" Guide') and ![logo](https://cdn.example.com/a.png 'Acme "logo"')`} + , + ); + expect(actualOutput).toMatchInlineSnapshot(` + "

guide and logo

+
" + `); + }); + it('renders lists in the correct format for browsers', async () => { const actualOutput = await render( diff --git a/packages/react-email/src/components/markdown/markdown.tsx b/packages/react-email/src/components/markdown/markdown.tsx index 0a2a1e7c97..9f47532c7e 100644 --- a/packages/react-email/src/components/markdown/markdown.tsx +++ b/packages/react-email/src/components/markdown/markdown.tsx @@ -98,7 +98,7 @@ export const Markdown = React.forwardRef( renderer.image = ({ href, text, title }) => { return `${text.replaceAll('( renderer.link = ({ href, title, tokens }) => { const text = renderer.parser.parseInline(tokens); - return `