TypeCellOS / TypeCellOS/BlockNote
Unescaped Text in `ReactEmailExporter` Causing HTML Injection / XSS and Output Corruption
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 10.2k
- Forks
- 772
- Avg merge
- 3d 11h
- Merged PRs (30d)
- 17
Description
What’s broken?
In @blocknote/xl-email-exporter, user text content is injected directly into dangerouslySetInnerHTML without HTML entity encoding.
Inside packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx, the method transformStyledText converts newlines to <br /> but passes the raw unescaped styledText.text directly into the DOM:
public transformStyledText(styledText: StyledText<S>) {
const stylesArray = this.mapStyles(styledText.styles);
const styles = Object.assign({}, ...stylesArray);
return (
<span
style={styles}
dangerouslySetInnerHTML={{
__html: styledText.text.replace(/\n/g, "<br />"),
}}
/>
);
}
This causes two critical defects:
- Security (HTML / XSS Injection): If an application exports user-generated BlockNote documents into emails, malicious payloads (e.g. <img src=x onerror=...>, <script>, or phishing HTML structures) are rendered verbatim in the generated email.
- Rendering & Layout Corruption: Standard text containing mathematical or programming comparison operators (e.g., 5 < 10 && 10 > 5, array[i < 5], or <CustomComponent> in documentation) is parsed by the email engine as raw HTML elements. This causes the text to either be stripped/hidden or break the email's DOM layout.
### What did you expect to happen?
All raw text characters (such as <, >, &, ", and ') should be properly HTML-escaped before inserting <br /> tags into dangerouslySetInnerHTML, ensuring:
- Safe rendering of untrusted user content.
- Visual preservation of literal < and > characters in exported email clients.
### Steps to reproduce
import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
import { ReactEmailExporter, reactEmailDefaultSchemaMappings } from "@blocknote/xl-email-exporter";
const schema = BlockNoteSchema.create({ blockSpecs: defaultBlockSpecs });
const exporter = new ReactEmailExporter(schema, reactEmailDefaultSchemaMappings);
const blocks = [
{
id: "block-1",
type: "paragraph" as const,
props: {},
content: [
{
type: "text" as const,
text: "Condition check: x < 10 & y > 20, or <script>alert(1)</script>",
styles: {},
},
],
children: [],
},
];
const emailHtml = await exporter.toReactEmailDocument(blocks);
console.log(emailHtml);
### BlockNote version
Version: 0.54.0 (and main branch) Package: @blocknote/xl-email-exporter
### Environment
OS: Any (Windows / macOS / Linux) Node.js: >=18.0.0 React: 18.x / 19.x Browser/Runtime: Node.js, Next.js, or browser export environments
### Additional context
Proposed Fix
Add an escapeHtml utility function and sanitize styledText.text before newline substitution:
--- a/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx
+++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx
@@ -24,6 +24,15 @@ import React, { CSSProperties } from "react";
+function escapeHtml(str: string): string {
- return str
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
+}
export class ReactEmailExporter<
B extends BlockSchema,
S extends StyleSchema,
@@ -66,7 +75,7 @@ export class ReactEmailExporter<
<span
style={styles}
dangerouslySetInnerHTML={{
-
__html: styledText.text.replace(/\n/g, "<br />"),
-
);__html: escapeHtml(styledText.text).replace(/\n/g, "<br />"), }} />
### Contribution
- [ ] I'd be interested in contributing a fix for this issue
### Sponsor
- [ ] I'm a [sponsor](https://www.blocknotejs.org/pricing) and would appreciate if you could look into this sooner than later 💖
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx at ReactEmailExporter.transformStyledText and inspect how styledText.text is passed to dangerouslySetInnerHTML. Ensure raw text is escaped before newline-to-br conversion, then verify the reproduction preserves literal comparison operators and does not render injected HTML or scripts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100