Unescaped Text in `ReactEmailExporter` Causing HTML Injection / XSS and Output Corruption
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 85/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- react, typescript
- Área
- security
Línea de trabajo
Empieza en packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx, en ReactEmailExporter.transformStyledText, e inspecciona cómo se pasa styledText.text a dangerouslySetInnerHTML. Asegúrate de que el texto sin procesar se escape antes de la conversión de saltos de línea a br y, después, verifica que la reproducción conserve los operadores de comparación literales y no renderice HTML ni scripts inyectados.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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 💖
- Lenguaje dominante
- TypeScript
- Estrellas
- 10.2k
- Forks
- 772
- Merge medio
- 7 d 21 h
- PR fusionados (30 d)
- 19
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de TypeCellOS/BlockNote
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
TypeCellOS/BlockNote#3098 ·
-
Off-By-One `RangeError` Crash and Text Duplication in `StyleManager.editLink` and `deleteLink` Abiertoneeds-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
TypeCellOS/BlockNote#3073 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
TypeCellOS/BlockNote#2949 · 1 comentario ·
-
a11y
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
TypeCellOS/BlockNote#2855 ·
-
a11y
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
TypeCellOS/BlockNote#2829 · 1 comentario ·
Todos los issues de TypeCellOS/BlockNote
Issues similares
-
S: triage
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
fix(errors): EHOSTUNREACH from a happy-eyeballs connect is reported as a resolver error (STAMP-80) Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 90/100
snapshot-labs/stamp#666 ·
-
fix(api): prevent leaderboard SSE heartbeat from starting after disconnect during initial load Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
GauravKarakoti/SecureFlow#1070 · 1 comentario ·
-
feature:Languages/Translations good first issue ready Web
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
digitalfabrik/integreat-app#4394 ·