TypeCellOS / TypeCellOS/BlockNote

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

Open Beginner friendly
#3,072 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.