(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "@n8n/tournament", "./errors", "./utils"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sanitizer = exports.PrototypeSanitizer = exports.DollarSignValidator = exports.DOLLAR_SIGN_ERROR = exports.sanitizerName = void 0; const tournament_1 = require("@n8n/tournament"); const errors_1 = require("./errors"); const utils_1 = require("./utils"); exports.sanitizerName = '__sanitize'; const sanitizerIdentifier = tournament_1.astBuilders.identifier(exports.sanitizerName); exports.DOLLAR_SIGN_ERROR = 'Cannot access "$" without calling it as a function'; /** * Helper to check if an expression is a valid property access with $ as the property. * Returns true for obj.$ or obj.nested.$ but false for bare $ or other expression contexts. */ const isValidDollarPropertyAccess = (expr) => { if (typeof expr !== 'object' || expr === null || !('type' in expr) || expr.type !== 'MemberExpression' || !('property' in expr) || !('object' in expr)) { return false; } const property = expr.property; const object = expr.object; // $ must be the property const isPropertyDollar = typeof property === 'object' && property !== null && 'name' in property && property.name === '$'; // $ must NOT be the object (to block $.something) const isObjectDollar = typeof object === 'object' && object !== null && 'name' in object && object.name === '$'; // Object must be an Identifier (obj) or MemberExpression (obj.nested) // This excludes bare $ or $ in other expression contexts const isObjectValid = typeof object === 'object' && object !== null && 'type' in object && (object.type === 'Identifier' || object.type === 'MemberExpression'); return isPropertyDollar && !isObjectDollar && isObjectValid; }; /** * Validates that the $ identifier is only used in allowed contexts. * This prevents user errors like `{{ $ }}` which would return the function object itself. * * Allowed contexts: * - As a function call: $() * - As a property name: obj.$ (where $ is a valid property name in JavaScript) * * Disallowed contexts: * - Bare identifier: $ * - As object in member expression: $.property * - In expressions: "prefix" + $, [1, 2, $], etc. */ const DollarSignValidator = (ast, _dataNode) => { (0, tournament_1.astVisit)(ast, { visitIdentifier(path) { this.traverse(path); const node = path.node; // Only check for the exact identifier '$' if (node.name !== '$') return; // Runtime type checking since path properties are typed as 'any' const parent = path.parent; // Check if parent is a path object with a 'name' property if (typeof parent !== 'object' || parent === null || !('name' in parent)) { throw new errors_1.ExpressionError(exports.DOLLAR_SIGN_ERROR); } // Allow $ when it's the callee: $() // parent.name === 'callee' means the parent path represents the callee field if (parent.name === 'callee') { return; } // Block when $ is the object in a MemberExpression: $.something // parent.name === 'object' means the parent path represents the object field if (parent.name === 'object') { throw new errors_1.ExpressionError(exports.DOLLAR_SIGN_ERROR); } // Check if $ is the property of a MemberExpression: obj.$ // For obj.$: parent.name is 'expression' and grandparent has ExpressionStatement // The ExpressionStatement should contain a MemberExpression with $ as property if ('parent' in parent && typeof parent.parent === 'object' && parent.parent !== null) { const grandparent = parent.parent; if ('value' in grandparent && typeof grandparent.value === 'object' && grandparent.value !== null) { const gpNode = grandparent.value; // ExpressionStatement has an 'expression' field containing the actual expression if ('type' in gpNode && gpNode.type === 'ExpressionStatement' && 'expression' in gpNode) { // Check if this is a valid property access like obj.$ if (isValidDollarPropertyAccess(gpNode.expression)) { return; } } } } // Disallow all other cases (bare $, $ in expressions, etc.) throw new errors_1.ExpressionError(exports.DOLLAR_SIGN_ERROR); }, }); }; exports.DollarSignValidator = DollarSignValidator; const PrototypeSanitizer = (ast, dataNode) => { (0, tournament_1.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') { throw new errors_1.ExpressionError(`Unknown property type ${node.property.type} while sanitising expression`); } if (!(0, utils_1.isSafeObjectProperty)(node.property.name)) { throw new errors_1.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 (!(0, utils_1.isSafeObjectProperty)(node.property.value)) { throw new errors_1.ExpressionError(`Cannot access "${node.property.value}" 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(tournament_1.astBuilders.memberExpression( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any node.object, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument tournament_1.astBuilders.callExpression(tournament_1.astBuilders.memberExpression(dataNode, sanitizerIdentifier), [ // eslint-disable-next-line @typescript-eslint/no-explicit-any node.property, ]), true)); } }, }); }; exports.PrototypeSanitizer = PrototypeSanitizer; const sanitizer = (value) => { if (!(0, utils_1.isSafeObjectProperty)(value)) { throw new errors_1.ExpressionError(`Cannot access "${value}" due to security concerns`); } return value; }; exports.sanitizer = sanitizer; }); //# sourceMappingURL=expression-sandboxing.js.map