From d18a29d5882fb8f4475258189f6badcd0a573b34 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Sat, 18 Jun 2022 08:09:37 +0300 Subject: [PATCH] fix(core): Updated expressions allowlist and denylist. (#3424) * feat: Updated expressions allowlist and denylist. * test: Added unit tests for expression allow and deny list. * feat: Updated riot-tmpl to be installed from n8n fork. * fix: Added check for non-standard browser built-in. * chore: Removed package-lock.json from branch. * chore: Removed package-lock.json from branch. * chore: Added jest-environment-jsdom@27 --- packages/workflow/package.json | 8 +- packages/workflow/src/Expression.ts | 101 ++++++++++++++- packages/workflow/test/Expression.test.ts | 145 ++++++++++++++++++++++ 3 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 packages/workflow/test/Expression.test.ts diff --git a/packages/workflow/package.json b/packages/workflow/package.json index f24011eb6e..02287f6421 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -21,7 +21,8 @@ "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow", "lintfix": "cd ../.. && node_modules/eslint/bin/eslint.js packages/workflow --fix", "watch": "tsc --watch", - "test": "jest" + "test": "jest", + "test:dev": "jest --watch" }, "files": [ "dist" @@ -32,8 +33,8 @@ "@types/jmespath": "^0.15.0", "@types/lodash.get": "^4.4.6", "@types/lodash.merge": "^4.6.6", - "@types/luxon": "^2.0.9", "@types/lodash.set": "^4.3.6", + "@types/luxon": "^2.0.9", "@types/node": "14.17.27", "@types/xml2js": "^0.4.3", "@typescript-eslint/eslint-plugin": "^4.29.0", @@ -44,6 +45,7 @@ "eslint-plugin-import": "^2.23.4", "eslint-plugin-prettier": "^3.4.0", "jest": "^27.4.7", + "jest-environment-jsdom": "^27.5.1", "prettier": "^2.3.2", "ts-jest": "^27.1.3", "tslint": "^6.1.2", @@ -56,7 +58,7 @@ "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "luxon": "^2.3.0", - "riot-tmpl": "^3.0.8", + "riot-tmpl": "github:n8n-io/tmpl", "xml2js": "^0.4.23" }, "jest": { diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index ac838541ae..d9b8cad649 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -1,3 +1,4 @@ +/* eslint-disable id-denylist */ // @ts-ignore import * as tmpl from 'riot-tmpl'; import { DateTime, Duration, Interval } from 'luxon'; @@ -125,12 +126,17 @@ export class Expression { versions: process.versions, }; + /** + * Denylist + */ + // @ts-ignore data.document = {}; data.global = {}; data.window = {}; data.Window = {}; data.this = {}; + data.globalThis = {}; data.self = {}; // Alerts @@ -140,6 +146,7 @@ export class Expression { // Prevent Remote Code Execution data.eval = {}; + data.uneval = {}; data.setTimeout = {}; data.setInterval = {}; data.Function = {}; @@ -148,13 +155,103 @@ export class Expression { data.fetch = {}; data.XMLHttpRequest = {}; + // Prevent control abstraction + data.Promise = {}; + data.Generator = {}; + data.GeneratorFunction = {}; + data.AsyncFunction = {}; + data.AsyncGenerator = {}; + data.AsyncGeneratorFunction = {}; + + // Prevent WASM + data.WebAssembly = {}; + + // Prevent Reflection + data.Reflect = {}; + data.Proxy = {}; + // @ts-ignore + data.constructor = {}; + + // Deprecated + data.escape = {}; + data.unescape = {}; + + /** + * Allowlist + */ + + // Dates + data.Date = Date; data.DateTime = DateTime; data.Interval = Interval; data.Duration = Duration; - // @ts-ignore - data.constructor = {}; + // Objects + data.Object = Object; + + // Arrays + data.Array = Array; + data.Int8Array = Int8Array; + data.Uint8Array = Uint8Array; + data.Uint8ClampedArray = Uint8ClampedArray; + data.Int16Array = Int16Array; + data.Uint16Array = Uint16Array; + data.Int32Array = Int32Array; + data.Uint32Array = Uint32Array; + data.Float32Array = Float32Array; + data.Float64Array = Float64Array; + data.BigInt64Array = typeof BigInt64Array !== 'undefined' ? BigInt64Array : {}; + data.BigUint64Array = typeof BigUint64Array !== 'undefined' ? BigUint64Array : {}; + + // Collections + data.Map = typeof Map !== 'undefined' ? Map : {}; + data.WeakMap = typeof WeakMap !== 'undefined' ? WeakMap : {}; + data.Set = typeof Set !== 'undefined' ? Set : {}; + data.WeakSet = typeof WeakSet !== 'undefined' ? WeakSet : {}; + + // Errors + data.Error = Error; + data.TypeError = TypeError; + data.SyntaxError = SyntaxError; + data.EvalError = EvalError; + data.RangeError = RangeError; + data.ReferenceError = ReferenceError; + data.URIError = URIError; + + // Internationalization + data.Intl = typeof Intl !== 'undefined' ? Intl : {}; + + // Text + data.String = String; + data.RegExp = RegExp; + + // Math + data.Math = Math; + data.Number = Number; + data.BigInt = typeof BigInt !== 'undefined' ? BigInt : {}; + data.Infinity = Infinity; + data.NaN = NaN; + data.isFinite = Number.isFinite; + data.isNaN = Number.isNaN; + data.parseFloat = parseFloat; + data.parseInt = parseInt; + + // Structured data + data.JSON = JSON; + data.ArrayBuffer = typeof ArrayBuffer !== 'undefined' ? ArrayBuffer : {}; + data.SharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined' ? SharedArrayBuffer : {}; + data.Atomics = typeof Atomics !== 'undefined' ? Atomics : {}; + data.DataView = typeof DataView !== 'undefined' ? DataView : {}; + + data.encodeURI = encodeURI; + data.encodeURIComponent = encodeURIComponent; + data.decodeURI = decodeURI; + data.decodeURIComponent = decodeURIComponent; + + // Other + data.Boolean = Boolean; + data.Symbol = Symbol; // Execute the expression // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts new file mode 100644 index 0000000000..8932ff30e3 --- /dev/null +++ b/packages/workflow/test/Expression.test.ts @@ -0,0 +1,145 @@ +/** + * @jest-environment jsdom + */ + +import { + Expression, + Workflow, +} from "../src"; +import * as Helpers from "./Helpers"; +import { + DateTime, + Duration, + Interval +} from "luxon"; + +describe('Expression', () => { + describe('getParameterValue()', () => { + const nodeTypes = Helpers.NodeTypes(); + const workflow = new Workflow({ nodes: [ + { + name: 'node', + typeVersion: 1, + type: 'test.set', + position: [0, 0], + parameters: {} + } + ], connections: {}, active: false, nodeTypes }); + const expression = new Expression(workflow); + + const evaluate = (value: string) => expression.getParameterValue( + value, + null, + 0, + 0, + 'node', + [], + 'manual', + '', + {}, + ); + + it('should not be able to use global built-ins from denylist', () => { + expect(evaluate('={{document}}')).toEqual({}); + expect(evaluate('={{window}}')).toEqual({}); + + expect(evaluate('={{Window}}')).toEqual({}); + expect(evaluate('={{globalThis}}')).toEqual({}); + expect(evaluate('={{self}}')).toEqual({}); + + expect(evaluate('={{alert}}')).toEqual({}); + expect(evaluate('={{prompt}}')).toEqual({}); + expect(evaluate('={{confirm}}')).toEqual({}); + + expect(evaluate('={{eval}}')).toEqual({}); + expect(evaluate('={{uneval}}')).toEqual({}); + expect(evaluate('={{setTimeout}}')).toEqual({}); + expect(evaluate('={{setInterval}}')).toEqual({}); + expect(evaluate('={{Function}}')).toEqual({}); + + expect(evaluate('={{fetch}}')).toEqual({}); + expect(evaluate('={{XMLHttpRequest}}')).toEqual({}); + + expect(evaluate('={{Promise}}')).toEqual({}); + expect(evaluate('={{Generator}}')).toEqual({}); + expect(evaluate('={{GeneratorFunction}}')).toEqual({}); + expect(evaluate('={{AsyncFunction}}')).toEqual({}); + expect(evaluate('={{AsyncGenerator}}')).toEqual({}); + expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({}); + + expect(evaluate('={{WebAssembly}}')).toEqual({}); + + expect(evaluate('={{Reflect}}')).toEqual({}); + expect(evaluate('={{Proxy}}')).toEqual({}); + + expect(evaluate('={{constructor}}')).toEqual({}); + + expect(evaluate('={{escape}}')).toEqual({}); + expect(evaluate('={{unescape}}')).toEqual({}); + }); + + it('should be able to use global built-ins from allowlist', () => { + expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date); + expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual(DateTime.now().toLocaleString()); + expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(Interval.after(new Date(), 100)); + expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100)); + + expect(evaluate('={{new Object()}}')).toEqual(new Object()); + + expect(evaluate('={{new Array()}}')).toEqual(new Array()); + expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); + expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); + expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); + expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array()); + expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array()); + expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array()); + expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array()); + expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array()); + expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array()); + expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array()); + expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array()); + + expect(evaluate('={{new Map()}}')).toEqual(new Map()); + expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap()); + expect(evaluate('={{new Set()}}')).toEqual(new Set()); + expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet()); + + expect(evaluate('={{new Error()}}')).toEqual(new Error()); + expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError()); + expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError()); + expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError()); + expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError()); + expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError()); + expect(evaluate('={{new URIError()}}')).toEqual(new URIError()); + + expect(evaluate('={{Intl}}')).toEqual(Intl); + + expect(evaluate('={{new String()}}')).toEqual(new String()); + expect(evaluate('={{new RegExp(\'\')}}')).toEqual(new RegExp('')); + + expect(evaluate('={{Math}}')).toEqual(Math); + expect(evaluate('={{new Number()}}')).toEqual(new Number()); + expect(evaluate('={{BigInt(\'1\')}}')).toEqual(BigInt('1')); + expect(evaluate('={{Infinity}}')).toEqual(Infinity); + expect(evaluate('={{NaN}}')).toEqual(NaN); + expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1)); + expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1)); + expect(evaluate('={{parseFloat(\'1\')}}')).toEqual(parseFloat('1')); + expect(evaluate('={{parseInt(\'1\', 10)}}')).toEqual(parseInt('1', 10)); + + expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({})); + expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10)); + expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10)); + expect(evaluate('={{Atomics}}')).toEqual(Atomics); + expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual(new DataView(new ArrayBuffer(1))); + + expect(evaluate('={{encodeURI(\'https://google.com\')}}')).toEqual(encodeURI('https://google.com')); + expect(evaluate('={{encodeURIComponent(\'https://google.com\')}}')).toEqual(encodeURIComponent('https://google.com')); + expect(evaluate('={{decodeURI(\'https://google.com\')}}')).toEqual(decodeURI('https://google.com')); + expect(evaluate('={{decodeURIComponent(\'https://google.com\')}}')).toEqual(decodeURIComponent('https://google.com')); + + expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1)); + expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString()); + }); + }); +})