fix: Preserve reserved tokens (#31363)

This commit is contained in:
yehorkardash 2026-05-29 16:29:28 +03:00 committed by GitHub
parent 9e41d03b29
commit dd4b3ff446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 128 additions and 50 deletions

View File

@ -46,4 +46,17 @@ describe('copyInputItems', () => {
expect(output[0].a).toEqual(input.a);
expect(output[0].a === input.a).toEqual(false);
});
it.each(['__proto__', 'constructor', 'prototype'])(
'should isolate items from inherited properties when given "%s"',
(propertyName) => {
const output = copyInputItems(
[{ json: { [propertyName]: 'test_value', safe: 1 } }],
[propertyName, 'safe'],
);
expect(output[0]).toHaveProperty('safe', 1);
expect(Object.getOwnPropertyNames(output[0])).toContain(propertyName);
},
);
});

View File

@ -7,7 +7,7 @@ import { deepCopy } from 'n8n-workflow';
*/
export function copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[] {
return items.map((item) => {
const newItem: IDataObject = {};
const newItem: IDataObject = Object.create(null) as IDataObject;
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;

View File

@ -2,16 +2,15 @@ import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
INodeExecutionData,
IHttpRequestMethods,
} from 'n8n-workflow';
import { ApplicationError, deepCopy } from 'n8n-workflow';
import { ApplicationError } from 'n8n-workflow';
import type { IRequestBody } from './types';
import { getAwsCredentials } from '../GenericFunctions';
import type { IRequestBody } from './types';
export async function awsApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
@ -94,16 +93,3 @@ export async function awsApiRequestAllItems(
return returnData;
}
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = deepCopy(item.json[property]);
}
}
return newItem;
}

View File

@ -1,5 +1,5 @@
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import { deepCopy, assert, ApplicationError } from 'n8n-workflow';
import type { IDataObject } from 'n8n-workflow';
import { ApplicationError, assert } from 'n8n-workflow';
import type {
AdjustedPutItem,
@ -102,19 +102,6 @@ export function validateJSON(input: any): object {
}
}
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = deepCopy(item.json[property]);
}
}
return newItem;
}
export function mapToAttributeValues(item: IDataObject): void {
for (const key of Object.keys(item)) {
if (!key.startsWith(':')) {

View File

@ -80,7 +80,7 @@ export class Bubble implements INodeType {
property: [{ key: string; value: string }];
};
const body = {} as IDataObject;
const body = Object.create(null) as IDataObject;
property.forEach((data) => (body[data.key] = data.value));
@ -164,7 +164,7 @@ export class Bubble implements INodeType {
property: [{ key: string; value: string }];
};
const body = {} as IDataObject;
const body = Object.create(null) as IDataObject;
property.forEach((data) => (body[data.key] = data.value));
responseData = await bubbleApiRequest.call(this, 'PATCH', endpoint, body, {});

View File

@ -16,7 +16,7 @@ import type { ITables, OperationInputData } from './interfaces';
*/
export function copyInputItem(item: INodeExecutionData, properties: string[]): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
const newItem: IDataObject = Object.create(null);
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
@ -44,20 +44,21 @@ export function createTableStruct(
const table = getNodeParam('table', index) as string;
const columnString = getNodeParam('columns', index) as string;
const columns = columnString.split(',').map((column) => column.trim());
const itemCopy = copyInputItem(item, columns.concat(additionalProperties));
const keyParam = keyName ? (getNodeParam(keyName, index) as string) : undefined;
if (tables[table] === undefined) {
tables[table] = {};
if (!Object.hasOwn(tables, table)) {
tables[table] = Object.create(null);
}
if (tables[table][columnString] === undefined) {
if (!Object.hasOwn(tables[table], columnString)) {
tables[table][columnString] = [];
}
if (keyName) {
itemCopy[keyName] = keyParam;
}
tables[table][columnString].push(itemCopy);
(tables[table][columnString] as IDataObject[]).push(itemCopy);
return tables;
}, {} as ITables);
}, Object.create(null) as ITables);
}
/**

View File

@ -384,15 +384,16 @@ export class MicrosoftSql implements INodeType {
const tables = items.reduce((acc, item, index) => {
const table = this.getNodeParameter('table', index) as string;
const deleteKey = this.getNodeParameter('deleteKey', index) as string;
if (acc[table] === undefined) {
acc[table] = {};
if (!Object.hasOwn(acc, table)) {
acc[table] = Object.create(null);
}
if (acc[table][deleteKey] === undefined) {
if (!Object.hasOwn(acc[table], deleteKey)) {
acc[table][deleteKey] = [];
}
acc[table][deleteKey].push(item);
(acc[table][deleteKey] as INodeExecutionData[]).push(item);
return acc;
}, {} as ITables);
}, Object.create(null) as ITables);
responseData = await deleteOperation(tables, pool);
}

View File

@ -178,4 +178,42 @@ describe('MicrosoftSql Node', () => {
}
},
);
describe('delete operation parameter validation', () => {
const buildPoolMock = () => {
const mockRequest = {
query: jest.fn().mockResolvedValue({ rowsAffected: [1] }),
input: jest.fn(),
};
const mockPool = mock<mssql.ConnectionPool>({
connect: jest.fn().mockResolvedValue(undefined),
close: jest.fn(),
request: jest.fn().mockReturnValue(mockRequest),
});
return mockPool;
};
it('should not alter shared object state when delete parameters contain reserved tokens', async () => {
const protoKeysBefore = Object.getOwnPropertyNames(Object.prototype);
mockedConnectionPool.mockReturnValue(buildPoolMock());
const node = new MicrosoftSql();
for (const reserved of ['__proto__', 'constructor', 'prototype']) {
const context = getMockedExecuteFunctions({
getInputData: jest.fn().mockReturnValue([{ json: { id: 1 }, pairedItem: { item: 0 } }]),
getNodeParameter: jest.fn().mockImplementation((paramName: string) => {
if (paramName === 'operation') return 'delete';
if (paramName === 'table') return reserved;
if (paramName === 'deleteKey') return 'id';
return undefined;
}),
continueOnFail: jest.fn().mockReturnValue(false),
});
await node.execute.call(context);
}
expect(Object.getOwnPropertyNames(Object.prototype)).toEqual(protoKeysBefore);
});
});
});

View File

@ -1,10 +1,12 @@
import { Request } from 'mssql';
import type { IResult } from 'mssql';
import type mssql from 'mssql';
import type { IDataObject } from 'n8n-workflow';
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import {
configurePool,
copyInputItem,
createTableStruct,
deleteOperation,
escapeIdentifier,
escapeTableName,
@ -429,4 +431,54 @@ describe('MSSQL tests', () => {
expect(escapeTableName('[schema].table')).toEqual('[[schema]].table]');
});
});
describe('createTableStruct', () => {
const makeItem = (json: IDataObject): INodeExecutionData => ({
json,
pairedItem: { item: 0 },
});
const makeGetParam =
(table: string, columns: string) =>
(param: string, _index: number): string => {
if (param === 'table') return table;
if (param === 'columns') return columns;
return '';
};
it('should build the correct struct for standard inputs', () => {
const items = [makeItem({ id: 1, name: 'Alice' })];
const result = createTableStruct(makeGetParam('users', 'id, name'), items);
expect(result).toEqual({
users: {
'id, name': [{ id: 1, name: 'Alice' }],
},
});
});
it('should group multiple items with the same table and columns', () => {
const items = [makeItem({ id: 1 }), makeItem({ id: 2 })];
const result = createTableStruct(makeGetParam('orders', 'id'), items);
expect(result.orders['id']).toHaveLength(2);
});
});
describe('copyInputItem', () => {
const makeItem = (json: IDataObject): INodeExecutionData => ({
json,
pairedItem: { item: 0 },
});
it('should copy specified properties from item json', () => {
const item = makeItem({ id: 1, name: 'Bob', age: 30 });
expect(copyInputItem(item, ['id', 'name'])).toEqual({ id: 1, name: 'Bob' });
});
it('should set missing properties to null', () => {
const item = makeItem({ id: 1 });
expect(copyInputItem(item, ['id', 'name'])).toEqual({ id: 1, name: null });
});
});
});

View File

@ -21,7 +21,7 @@ export function getItemsCopy(
): IDataObject[] {
let newItem: IDataObject;
return items.map((item) => {
newItem = {};
newItem = Object.create(null) as IDataObject;
if (guardedColumns) {
Object.keys(guardedColumns).forEach((column) => {
newItem[column] = item.json[guardedColumns[column]];
@ -47,7 +47,7 @@ export function getItemCopy(
properties: string[],
guardedColumns?: { [key: string]: string },
): IDataObject {
const newItem: IDataObject = {};
const newItem: IDataObject = Object.create(null) as IDataObject;
if (guardedColumns) {
Object.keys(guardedColumns).forEach((column) => {
newItem[column] = item.json[guardedColumns[column]];