fix(core): Filter out prototype and constructor lookups in expressions (#10382)

This commit is contained in:
Val
2024-08-13 16:57:01 +01:00
committed by GitHub
parent 117e2d968f
commit 8e7d29ad3c
7 changed files with 162 additions and 10 deletions

View File

@@ -26,6 +26,7 @@ import { extendSyntax } from './Extensions/ExpressionExtension';
import { evaluateExpression, setErrorHandler } from './ExpressionEvaluatorProxy';
import { getGlobalState } from './GlobalState';
import { ApplicationError } from './errors/application.error';
import { sanitizer, sanitizerName } from './ExpressionSandboxing';
const IS_FRONTEND_IN_DEV_MODE =
typeof process === 'object' &&
@@ -306,6 +307,8 @@ export class Expression {
data.extend = extend;
data.extendOptional = extendOptional;
data[sanitizerName] = sanitizer;
Object.assign(data, extendedFunctions);
const constructorValidation = new RegExp(/\.\s*constructor/gm);

View File

@@ -3,6 +3,7 @@ import type { ReturnValue, TmplDifference } from '@n8n/tournament';
import { Tournament } from '@n8n/tournament';
import type { ExpressionEvaluatorType } from './Interfaces';
import * as LoggerProxy from './LoggerProxy';
import { PrototypeSanitizer } from './ExpressionSandboxing';
type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue;
type ErrorHandler = (error: Error) => void;
@@ -18,6 +19,7 @@ const differenceChecker = (diff: TmplDifference) => {
if (diff.same) {
return;
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (diff.has?.function || diff.has?.templateString) {
return;
}
@@ -30,7 +32,10 @@ const differenceChecker = (diff: TmplDifference) => {
LoggerProxy.error('Expression evaluator difference checker failed');
}
};
const tournamentEvaluator = new Tournament(errorHandler, undefined);
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [],
after: [PrototypeSanitizer],
});
let evaluator: Evaluator = tmpl.tmpl;
let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl';
let diffExpressions = false;

View File

@@ -0,0 +1,58 @@
import { type ASTAfterHook, astBuilders as b, astVisit } from '@n8n/tournament';
import { ExpressionError } from './errors';
const forbiddenMembers = ['__proto__', 'prototype', 'constructor', 'getPrototypeOf'];
export const sanitizerName = '__sanitize';
const sanitizerIdentifier = b.identifier(sanitizerName);
export const PrototypeSanitizer: ASTAfterHook = (ast, dataNode) => {
astVisit(ast, {
visitMemberExpression(path) {
this.traverse(path);
const node = path.node;
if (!node.computed) {
// This is static, so we're safe to error here
if (node.property.type !== 'Identifier') {
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new ExpressionError(
`Unknown property type ${node.property.type} while sanitising expression`,
);
}
if (forbiddenMembers.includes(node.property.name)) {
throw new ExpressionError(
`Cannot access "${node.property.name}" due to security concerns`,
);
}
} else if (node.property.type === 'StringLiteral' || node.property.type === 'Literal') {
// Check any static strings against our forbidden list
if (forbiddenMembers.includes(node.property.value as string)) {
throw new ExpressionError(
`Cannot access "${node.property.value as string}" due to security concerns`,
);
}
} else if (!node.property.type.endsWith('Literal')) {
// This isn't a literal value, so we need to wrap it
path.replace(
b.memberExpression(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
node.object as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
b.callExpression(b.memberExpression(dataNode, sanitizerIdentifier), [
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node.property as any,
]),
true,
),
);
}
},
});
};
export const sanitizer = (value: unknown): unknown => {
if (forbiddenMembers.includes(value as string)) {
throw new ExpressionError(`Cannot access "${value as string}" due to security concerns`);
}
return value;
};