diff --git a/packages/core/package.json b/packages/core/package.json
index 7dc33cc78a..e7ddf8f25b 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -55,10 +55,10 @@
"fast-glob": "catalog:",
"file-type": "16.5.4",
"form-data": "catalog:",
+ "htmlparser2": "^10.0.0",
"http-proxy-agent": "catalog:",
"https-proxy-agent": "catalog:",
"iconv-lite": "catalog:",
- "jsdom": "23.0.1",
"jsonwebtoken": "catalog:",
"lodash": "catalog:",
"luxon": "catalog:",
diff --git a/packages/core/src/__tests__/html-sandbox.test.ts b/packages/core/src/__tests__/html-sandbox.test.ts
index d93a9e8bf4..2194de2fbd 100644
--- a/packages/core/src/__tests__/html-sandbox.test.ts
+++ b/packages/core/src/__tests__/html-sandbox.test.ts
@@ -6,6 +6,7 @@ import { Readable } from 'stream';
import {
bufferEscapeHtml,
createHtmlSandboxTransformStream,
+ hasHtml,
isHtmlRenderedContentType,
sandboxHtmlResponse,
} from '../html-sandbox';
@@ -373,3 +374,21 @@ describe('sandboxHtmlResponse > sandboxing disabled', () => {
expect(sandboxHtmlResponse(data)).toEqual(data);
});
});
+
+describe('hasHtml', () => {
+ test('returns true for valid HTML', () => {
+ expect(hasHtml('
Hello
')).toBe(true);
+ });
+
+ test('returns true for malformed but still HTML-like content', () => {
+ expect(hasHtml('Test')).toBe(true);
+ });
+
+ test('returns false for plain text', () => {
+ expect(hasHtml('Just a string')).toBe(false);
+ });
+
+ test('returns false for empty string', () => {
+ expect(hasHtml('')).toBe(false);
+ });
+});
diff --git a/packages/core/src/html-sandbox.ts b/packages/core/src/html-sandbox.ts
index db55546b3f..5b8402558e 100644
--- a/packages/core/src/html-sandbox.ts
+++ b/packages/core/src/html-sandbox.ts
@@ -1,6 +1,6 @@
import { SecurityConfig } from '@n8n/config';
import { Container } from '@n8n/di';
-import { JSDOM } from 'jsdom';
+import { ElementType, parseDocument } from 'htmlparser2';
import type { TransformCallback } from 'stream';
import { Transform } from 'stream';
@@ -13,10 +13,8 @@ export const isIframeSandboxDisabled = () => {
*/
export const hasHtml = (str: string) => {
try {
- const dom = new JSDOM(str);
- return (
- dom.window.document.body.children.length > 0 || dom.window.document.head.children.length > 0
- );
+ const doc = parseDocument(str);
+ return doc.children.some((node) => node.type === ElementType.Tag);
} catch {
return false;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2843b76297..8f8c167dba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1810,6 +1810,9 @@ importers:
form-data:
specifier: 4.0.4
version: 4.0.4
+ htmlparser2:
+ specifier: ^10.0.0
+ version: 10.0.0
http-proxy-agent:
specifier: 'catalog:'
version: 7.0.2
@@ -1819,9 +1822,6 @@ importers:
iconv-lite:
specifier: 'catalog:'
version: 0.6.3
- jsdom:
- specifier: 23.0.1
- version: 23.0.1
jsonwebtoken:
specifier: 'catalog:'
version: 9.0.2
@@ -9778,6 +9778,9 @@ packages:
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
@@ -9898,14 +9901,14 @@ packages:
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
engines: {node: '>=0.12'}
- entities@4.4.0:
- resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
- engines: {node: '>=0.12'}
-
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -11014,6 +11017,9 @@ packages:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
+ htmlparser2@10.0.0:
+ resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
+
htmlparser2@5.0.1:
resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
@@ -24938,7 +24944,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
- entities: 4.4.0
+ entities: 4.5.0
domain-browser@4.22.0: {}
@@ -24974,6 +24980,12 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
@@ -25110,10 +25122,10 @@ snapshots:
entities@3.0.1: {}
- entities@4.4.0: {}
-
entities@4.5.0: {}
+ entities@6.0.1: {}
+
env-paths@2.2.1: {}
epub2@3.0.2(ts-toolbelt@9.6.0):
@@ -26626,6 +26638,13 @@ snapshots:
htmlparser2: 8.0.2
selderee: 0.11.0
+ htmlparser2@10.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 6.0.1
+
htmlparser2@5.0.1:
dependencies:
domelementtype: 2.3.0