From 6c9c720ae9496d5f1ea9817241fe257d6be5a10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 23 Apr 2025 14:41:48 +0200 Subject: [PATCH] fix(Code Node): Upgrade pyodide, sandbox it, and prevent JS sandbox escape (#14356) --- package.json | 1 - packages/nodes-base/nodes/Code/Pyodide.ts | 17 ++++++- packages/nodes-base/package.json | 13 ++--- .../nodes-base/types/xmlhttprequest-ssl.d.ts | 3 ++ patches/pyodide@0.23.4.patch | 20 -------- pnpm-lock.yaml | 47 +++++++------------ 6 files changed, 42 insertions(+), 59 deletions(-) create mode 100644 packages/nodes-base/types/xmlhttprequest-ssl.d.ts delete mode 100644 patches/pyodide@0.23.4.patch diff --git a/package.json b/package.json index c74432f8ee..1cfbc3dfd2 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "patchedDependencies": { "bull@4.12.1": "patches/bull@4.12.1.patch", "pkce-challenge@5.0.0": "patches/pkce-challenge@5.0.0.patch", - "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@5.0.6": "patches/@types__express-serve-static-core@5.0.6.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch", diff --git a/packages/nodes-base/nodes/Code/Pyodide.ts b/packages/nodes-base/nodes/Code/Pyodide.ts index 42b70ea231..d430f15ef9 100644 --- a/packages/nodes-base/nodes/Code/Pyodide.ts +++ b/packages/nodes-base/nodes/Code/Pyodide.ts @@ -1,4 +1,5 @@ import { dirname } from 'node:path'; +import { createContext, runInContext } from 'node:vm'; import type { PyodideInterface } from 'pyodide'; let pyodideInstance: PyodideInterface | undefined; @@ -6,8 +7,22 @@ let pyodideInstance: PyodideInterface | undefined; export async function LoadPyodide(packageCacheDir: string): Promise { if (pyodideInstance === undefined) { const { loadPyodide } = await import('pyodide'); + const { XMLHttpRequest } = await import('xmlhttprequest-ssl'); const indexURL = dirname(require.resolve('pyodide')); - pyodideInstance = await loadPyodide({ indexURL, packageCacheDir }); + const context = createContext({ + loadPyodide, + indexURL, + packageCacheDir, + jsglobals: { + Object, + console, + XMLHttpRequest, + }, + }); + pyodideInstance = (await runInContext( + 'loadPyodide({ indexURL, packageCacheDir, jsglobals })', + context, + )) as PyodideInterface; await pyodideInstance.runPythonAsync(` from _pyodide_core import jsproxy_typedict diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 02b377d755..a8c51623ee 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -845,8 +845,8 @@ "@types/cheerio": "^0.22.15", "@types/eventsource": "^1.1.2", "@types/express": "catalog:", - "@types/html-to-text": "^9.0.1", "@types/gm": "^1.25.0", + "@types/html-to-text": "^9.0.1", "@types/js-nacl": "^1.3.0", "@types/jsonwebtoken": "catalog:", "@types/lodash": "catalog:", @@ -869,11 +869,11 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "3.666.0", "@kafkajs/confluent-schema-registry": "3.8.0", + "@mozilla/readability": "0.6.0", "@n8n/config": "workspace:*", "@n8n/di": "workspace:*", "@n8n/imap": "workspace:*", "@n8n/vm2": "3.9.25", - "@mozilla/readability": "0.6.0", "alasql": "4.4.0", "amqplib": "0.10.3", "aws4": "1.11.0", @@ -887,8 +887,8 @@ "eventsource": "2.0.2", "fast-glob": "catalog:", "fflate": "0.7.4", - "get-system-fonts": "2.0.2", "generate-schema": "2.6.0", + "get-system-fonts": "2.0.2", "gm": "1.25.1", "html-to-text": "9.0.5", "iconv-lite": "catalog:", @@ -916,10 +916,10 @@ "nodemailer": "6.9.9", "otpauth": "9.1.1", "pdfjs-dist": "2.16.105", - "pg-promise": "11.9.1", "pg": "8.12.0", + "pg-promise": "11.9.1", "promise-ftp": "1.3.5", - "pyodide": "0.23.4", + "pyodide": "0.27.5", "redis": "4.6.14", "rfc2047": "4.0.1", "rhea": "1.0.24", @@ -935,6 +935,7 @@ "ts-ics": "1.2.2", "uuid": "catalog:", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz", - "xml2js": "catalog:" + "xml2js": "catalog:", + "xmlhttprequest-ssl": "3.1.0" } } diff --git a/packages/nodes-base/types/xmlhttprequest-ssl.d.ts b/packages/nodes-base/types/xmlhttprequest-ssl.d.ts new file mode 100644 index 0000000000..2619286d3c --- /dev/null +++ b/packages/nodes-base/types/xmlhttprequest-ssl.d.ts @@ -0,0 +1,3 @@ +declare module 'xmlhttprequest-ssl' { + export const XMLHttpRequest: typeof globalThis.XMLHttpRequest; +} diff --git a/patches/pyodide@0.23.4.patch b/patches/pyodide@0.23.4.patch deleted file mode 100644 index efb5318177..0000000000 --- a/patches/pyodide@0.23.4.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/pyodide.d.ts b/pyodide.d.ts -index d5ed46f6345855a75ec6f2b7ef73237a0af64a7e..e0087c83792558ca30713e9d07a9a37625f68d8d 100644 ---- a/pyodide.d.ts -+++ b/pyodide.d.ts -@@ -1118,6 +1118,15 @@ export declare function loadPyodide(options?: { - * (``pyodide.js`` or ``pyodide.mjs``) removed. - */ - indexURL?: string; -+ /** -+ * The file path where packages will be cached in `node.js`. If a package -+ * exists in `packageCacheDir` it is loaded from there, otherwise it is -+ * downloaded from the JsDelivr CDN and then cached into `packageCacheDir`. -+ * Only applies when running in node.js. Ignored in browsers. -+ * -+ * Default: same as indexURL -+ */ -+ packageCacheDir?: string; - /** - * file. You can produce custom lock files with :py:func:`micropip.freeze`. - * Default: ```${indexURL}/repodata.json``` \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9235fe142c..26a0c7de1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,9 +182,6 @@ patchedDependencies: pkce-challenge@5.0.0: hash: 651e785d0b7bbf5be9210e1e895c39a16dc3ce8a5a3843b4819565fb6e175b90 path: patches/pkce-challenge@5.0.0.patch - pyodide@0.23.4: - hash: c1002dacf7f6d0827d23aaf6cf2845e1b0c351339306c4ad660b8cd72077976c - path: patches/pyodide@0.23.4.patch vue-tsc@2.2.8: hash: e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f path: patches/vue-tsc@2.2.8.patch @@ -2182,8 +2179,8 @@ importers: specifier: 1.3.5 version: 1.3.5(promise-ftp-common@1.1.5) pyodide: - specifier: 0.23.4 - version: 0.23.4(patch_hash=c1002dacf7f6d0827d23aaf6cf2845e1b0c351339306c4ad660b8cd72077976c)(encoding@0.1.13) + specifier: 0.27.5 + version: 0.27.5 redis: specifier: 4.6.14 version: 4.6.14 @@ -2232,6 +2229,9 @@ importers: xml2js: specifier: 'catalog:' version: 0.6.2 + xmlhttprequest-ssl: + specifier: 3.1.0 + version: 3.1.0 devDependencies: '@n8n/typescript-config': specifier: workspace:* @@ -7088,9 +7088,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -10929,6 +10926,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-ensure@0.0.0: resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==} @@ -10937,15 +10935,6 @@ packages: resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} engines: {node: 4.x || >=6.0.0} - node-fetch@2.6.8: - resolution: {integrity: sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -11804,8 +11793,9 @@ packages: pure-rand@6.0.1: resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==} - pyodide@0.23.4: - resolution: {integrity: sha512-WpQUHaIXQ1xede5BMqPAjBcmopxN22s5hEsYOR8T7/UW/fkNLFUn07SaemUgthbtvedD5JGymMMj4VpD9sGMTg==} + pyodide@0.27.5: + resolution: {integrity: sha512-nXErpLzEdtQolt+sNQ/5mKuN9XTUwhxR2MRhRhZ6oDRGpYLXrOp5+kkTPGEwK+wn1ZA8+poNmoxKTj2sq/p9og==} + engines: {node: '>=18.0.0'} python-struct@1.1.3: resolution: {integrity: sha512-UsI/mNvk25jRpGKYI38Nfbv84z48oiIWwG67DLVvjRhy8B/0aIK+5Ju5WOHgw/o9rnEmbAS00v4rgKFQeC332Q==} @@ -13875,6 +13865,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@3.1.0: + resolution: {integrity: sha512-UsofFE/khRRAcM9c3FGDEUSwupaQQC3Kme1brtz+B3N+RZHXGbD6AG6QzgWcunHzszqtOSMiZoPNrmHEBB2DjA==} + engines: {node: '>=12.0.0'} + xmllint-wasm@3.0.1: resolution: {integrity: sha512-t+aKQXJQNAt9/qLgCjhHUmCnPXAyqBKiyh8oV0ZwBMar/uB+5F40tqOJZ97JwLADcqQr5WB2bjCxLKrm+DHz1g==} engines: {node: '>=10.5.0'} @@ -20207,8 +20201,6 @@ snapshots: balanced-match@1.0.2: {} - base-64@1.0.0: {} - base64-js@1.5.1: {} basic-auth@2.0.1: @@ -25004,12 +24996,6 @@ snapshots: dependencies: http2-client: 1.3.5 - node-fetch@2.6.8(encoding@0.1.13): - dependencies: - whatwg-url: 5.0.0 - optionalDependencies: - encoding: 0.1.13 - node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -25935,14 +25921,11 @@ snapshots: pure-rand@6.0.1: {} - pyodide@0.23.4(patch_hash=c1002dacf7f6d0827d23aaf6cf2845e1b0c351339306c4ad660b8cd72077976c)(encoding@0.1.13): + pyodide@0.27.5: dependencies: - base-64: 1.0.0 - node-fetch: 2.6.8(encoding@0.1.13) ws: 8.17.1 transitivePeerDependencies: - bufferutil - - encoding - utf-8-validate python-struct@1.1.3: @@ -28345,6 +28328,8 @@ snapshots: xmlchars@2.2.0: {} + xmlhttprequest-ssl@3.1.0: {} + xmllint-wasm@3.0.1: {} xpath@0.0.32: {}