Skip to content

fix(xl-email-exporter): escape text instead of injecting it as HTML - #3100

Open
adarshsm wants to merge 1 commit into
TypeCellOS:mainfrom
adarshsm:fix/email-exporter-escape-text
Open

adarshsm wants to merge 1 commit into
TypeCellOS:mainfrom
adarshsm:fix/email-exporter-escape-text

Conversation

@adarshsm

@adarshsm adarshsm commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #3072

The bug

ReactEmailExporter.transformStyledText passed the document's text straight into dangerouslySetInnerHTML:

<span
  style={styles}
  dangerouslySetInnerHTML={{
    __html: styledText.text.replace(/\n/g, "<br />"),
  }}
/>

The replace call is only there to turn newlines into line breaks, but using dangerouslySetInnerHTML to do it means every other character in the text is interpreted as markup too. Two consequences:

  • HTML injection. Exporting a document whose text is <script>alert(1)</script> emits that as a live element in the email, not as text. Any application that mails out user-authored BlockNote documents forwards whatever markup the author typed.
  • Ordinary text is corrupted. x < 10 & y > 20, array[i < 5], or <CustomComponent> in a documentation snippet get parsed as tags by the email client and disappear or break the layout.

Added as a failing test against main first — the exported HTML contained <script> verbatim:

AssertionError: expected '<!DOCTYPE html PUBLIC "-//W3C//DTD XH…' not to contain '<script>'
...<span>x < 10 & y > 20<br /><script>alert(1)</script></span>...

The fix

Split on the newline and render the lines as React children with <br /> between them, so the raw-HTML escape hatch is no longer needed at all and React escapes the text:

<span style={styles}>
  {styledText.text.split("\n").map((line, index) => (
    <React.Fragment key={index}>
      {index > 0 && <br />}
      {line}
    </React.Fragment>
  ))}
</span>

I preferred this over adding an escapeHtml helper in front of the existing dangerouslySetInnerHTML (as the issue suggests): the escape hatch is what makes this class of bug possible, and dropping it removes the need for a hand-maintained escaping table.

I checked the other dangerouslySetInnerHTML call sites in packages/. The math-block ones render KaTeX-produced MathML and the xl-odt-exporter ones inject a developer-supplied header/footer, so neither is the same "document text treated as markup" case. This was the only one.

Notes on the snapshot churn

Four existing snapshots move. Both changes are serialization-only and render identically:

  • <br /><br/> — React's void-element output instead of the hand-written string.
  • '&#x27; in the inline-code text var foo = 'bar'; — React escaping the apostrophe.

I diffed the old and new snapshots character by character to confirm nothing else moved.

Testing

vp run --filter @blocknote/xl-email-exporter test
  Test Files  2 passed (2)
       Tests  23 passed (23)

vp lint on the package reports no new findings for the changed files (the 26 pre-existing errors are all in defaultSchema/blocks.tsx and untouched here). I did not run the e2e suite, since it needs the Docker runner.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved email export safety by displaying text containing HTML-like characters as plain text instead of interpreting it as markup.
    • Preserved line breaks in styled text so exported emails maintain the intended formatting.
    • Added coverage for special characters, script-like content, and multiline text rendering.

`transformStyledText` handed `styledText.text` to `dangerouslySetInnerHTML`
after replacing newlines with `<br />`, so any markup in the document text
reached the exported email verbatim: `<script>alert(1)</script>` stayed a
live element, and ordinary text such as `x < 10 & y > 20` was swallowed by
the email client's HTML parser.

Split on the newline instead and render the lines as React children with
`<br />` between them. React escapes text children, so the newline handling
no longer requires the raw-HTML escape hatch.

The snapshot updates are serialization-only: `<br />` -> `<br/>` and
`'` -> `&#x27;`, both of which render identically.

Fixes TypeCellOS#3072

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 20, 2026

Copy link
Copy Markdown

@adarshsm is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #3072 requires safe export of user-authored text and preservation of literal <, >, and & characters while retaining newline-to-line-break rendering. transformStyledText now renders text …
Out of Scope Changes check ✅ Passed The reviewed changes modify transformStyledText and add a focused regression test. The changes directly support issue #3072. Snapshot updates described in the pull request summary are serialization-…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Title check ✅ Passed The title clearly and concisely describes the main change: escaping text instead of injecting it as HTML in the email exporter.
Description check ✅ Passed The description is detailed and relevant. It explains the bug, rationale, implementation, snapshot changes, and testing results. It does not use the provided section headings or include the checklist,…

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@3100

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@3100

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@3100

@blocknote/diagram-block

npm i https://pkg.pr.new/@blocknote/diagram-block@3100

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@3100

@blocknote/math-block

npm i https://pkg.pr.new/@blocknote/math-block@3100

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@3100

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@3100

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@3100

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@3100

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@3100

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@3100

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@3100

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@3100

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@3100

@blocknote/xl-typst-exporter

npm i https://pkg.pr.new/@blocknote/xl-typst-exporter@3100

commit: 3a28606

@nperez0111
nperez0111 marked this pull request as ready for review September 22, 2026 10:03

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unescaped Text in ReactEmailExporter Causing HTML Injection / XSS and Output Corruption

1 participant