Skip to content

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

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

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

Conversation

@adarshsm

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

`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

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

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