Skip to content

Commit eceee7f

Browse files
authored
fix(core): Prevent prototype pollution of internal classes in task runner (#12610)
1 parent 4a1a999 commit eceee7f

File tree

4 files changed

+47
-17
lines changed

4 files changed

+47
-17
lines changed

Diff for: packages/@n8n/task-runner/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@
4040
"acorn": "8.14.0",
4141
"acorn-walk": "8.3.4",
4242
"lodash": "catalog:",
43+
"luxon": "catalog:",
4344
"n8n-core": "workspace:*",
4445
"n8n-workflow": "workspace:*",
4546
"nanoid": "catalog:",
4647
"ws": "^8.18.0"
4748
},
4849
"devDependencies": {
49-
"@types/lodash": "catalog:",
50-
"luxon": "catalog:"
50+
"@types/lodash": "catalog:"
5151
}
5252
}

Diff for: packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DateTime } from 'luxon';
1+
import { DateTime, Duration, Interval } from 'luxon';
22
import type { IBinaryData } from 'n8n-workflow';
33
import { setGlobalState, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
44
import fs from 'node:fs';
@@ -1412,5 +1412,28 @@ describe('JsTaskRunner', () => {
14121412
expect(outcome.result).toEqual([wrapIntoJson({ prototypeChanged: false })]);
14131413
checkPrototypeIntact();
14141414
});
1415+
1416+
test('should freeze luxon prototypes', async () => {
1417+
const outcome = await executeForAllItems({
1418+
code: `
1419+
[DateTime, Interval, Duration]
1420+
.forEach(constructor => {
1421+
constructor.prototype.maliciousKey = 'value';
1422+
});
1423+
1424+
return []
1425+
`,
1426+
inputItems: [{ a: 1 }],
1427+
});
1428+
1429+
expect(outcome.result).toEqual([]);
1430+
1431+
// @ts-expect-error Non-existing property
1432+
expect(DateTime.now().maliciousKey).toBeUndefined();
1433+
// @ts-expect-error Non-existing property
1434+
expect(Interval.fromISO('P1Y2M10DT2H30M').maliciousKey).toBeUndefined();
1435+
// @ts-expect-error Non-existing property
1436+
expect(Duration.fromObject({ hours: 1 }).maliciousKey).toBeUndefined();
1437+
});
14151438
});
14161439
});

Diff for: packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import set from 'lodash/set';
2+
import { DateTime, Duration, Interval } from 'luxon';
23
import { getAdditionalKeys } from 'n8n-core';
3-
import { WorkflowDataProxy, Workflow, ObservableObject } from 'n8n-workflow';
4+
import { WorkflowDataProxy, Workflow, ObservableObject, Expression } from 'n8n-workflow';
45
import type {
56
CodeExecutionMode,
67
IWorkflowExecuteAdditionalData,
@@ -108,15 +109,21 @@ export class JsTaskRunner extends TaskRunner {
108109
}
109110

110111
private preventPrototypePollution() {
111-
if (process.env.NODE_ENV === 'test') return; // needed for Jest
112-
113-
Object.getOwnPropertyNames(globalThis)
114-
// @ts-expect-error globalThis does not have string in index signature
115-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
116-
.map((name) => globalThis[name])
117-
.filter((value) => typeof value === 'function')
118-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
119-
.forEach((fn) => Object.freeze(fn.prototype));
112+
// Freeze globals, except for Jest
113+
if (process.env.NODE_ENV !== 'test') {
114+
Object.getOwnPropertyNames(globalThis)
115+
// @ts-expect-error globalThis does not have string in index signature
116+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
117+
.map((name) => globalThis[name])
118+
.filter((value) => typeof value === 'function')
119+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
120+
.forEach((fn) => Object.freeze(fn.prototype));
121+
}
122+
123+
// Freeze internal classes
124+
[Workflow, Expression, WorkflowDataProxy, DateTime, Interval, Duration]
125+
.map((constructor) => constructor.prototype)
126+
.forEach(Object.freeze);
120127
}
121128

122129
async executeTask(
@@ -478,7 +485,7 @@ export class JsTaskRunner extends TaskRunner {
478485
* @param dataProxy The data proxy object that provides access to built-ins
479486
* @param additionalProperties Additional properties to add to the context
480487
*/
481-
private buildContext(
488+
buildContext(
482489
taskId: string,
483490
workflow: Workflow,
484491
node: INode,

Diff for: pnpm-lock.yaml

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)