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