Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Microsoft SQL Node): Prevent SQL injection #7467

Merged
merged 11 commits into from
Oct 24, 2023
173 changes: 113 additions & 60 deletions packages/nodes-base/nodes/Microsoft/Sql/GenericFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import type { ITables } from './TableInterface';
import type { ITables, OperationInputData } from './interfaces';
import { chunk, flatten } from '@utils/utilities';
import mssql from 'mssql';

/**
* Returns a copy of the item which only contains the json data and
Expand Down Expand Up @@ -30,6 +31,7 @@ export function copyInputItem(item: INodeExecutionData, properties: string[]): I
* @param {function} getNodeParam getter for the Node's Parameters
*/
export function createTableStruct(
// eslint-disable-next-line @typescript-eslint/ban-types
getNodeParam: Function,
items: INodeExecutionData[],
additionalProperties: string[] = [],
Expand Down Expand Up @@ -61,10 +63,9 @@ export function createTableStruct(
* @param {ITables} tables The ITables to be processed.
* @param {function} buildQueryQueue function that builds the queue of promises
*/

export async function executeQueryQueue(
tables: ITables,
buildQueryQueue: Function,
buildQueryQueue: (data: OperationInputData) => Array<Promise<object>>,
): Promise<any[]> {
return Promise.all(
Object.keys(tables).map(async (table) => {
Expand All @@ -82,68 +83,120 @@ export async function executeQueryQueue(
);
}

/**
* Extracts the values from the item for INSERT
*
* @param {IDataObject} item The item to extract
*/
export function extractValues(item: IDataObject): string {
return `(${Object.values(item)
.map((val) => {
//the column cannot be found in the input
//so, set it to null in the sql query
if (val === null) {
return 'NULL';
} else if (typeof val === 'string') {
return `'${val.replace(/'/g, "''")}'`;
} else if (typeof val === 'boolean') {
return +!!val;
}
return val;
}) // maybe other types such as dates have to be handled as well
.join(',')})`;
export function formatColumns(columns: string) {
return columns
.split(',')
.map((column) => `[${column.trim()}]`)
.join(', ');
}

/**
* Extracts the SET from the item for UPDATE
*
* @param {IDataObject} item The item to extract from
* @param {string[]} columns The columns to update
*/
export function extractUpdateSet(item: IDataObject, columns: string[]): string {
return columns
.map(
(column) =>
`"${column}" = ${typeof item[column] === 'string' ? `'${item[column]}'` : item[column]}`,
)
.join(',');
export function configurePool(credentials: IDataObject) {
const config = {
server: credentials.server as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
domain: credentials.domain ? (credentials.domain as string) : undefined,
connectionTimeout: credentials.connectTimeout as number,
requestTimeout: credentials.requestTimeout as number,
options: {
encrypt: credentials.tls as boolean,
enableArithAbort: false,
tdsVersion: credentials.tdsVersion as string,
trustServerCertificate: credentials.allowUnauthorizedCerts as boolean,
},
};

return new mssql.ConnectionPool(config);
}

/**
* Extracts the WHERE condition from the item for UPDATE
*
* @param {IDataObject} item The item to extract from
* @param {string} key The column name to build the condition with
*/
export function extractUpdateCondition(item: IDataObject, key: string): string {
return `${key} = ${typeof item[key] === 'string' ? `'${item[key]}'` : item[key]}`;
export async function insertOperation(tables: ITables, pool: mssql.ConnectionPool) {
return executeQueryQueue(
tables,
({ table, columnString, items }: OperationInputData): Array<Promise<object>> => {
return chunk(items, 1000).map(async (insertValues) => {
const request = pool.request();

const valuesPlaceholder = [];

for (const [rIndex, entry] of insertValues.entries()) {
const row = Object.values(entry);
valuesPlaceholder.push(`(${row.map((_, vIndex) => `@r${rIndex}v${vIndex}`).join(', ')})`);
for (const [vIndex, value] of row.entries()) {
request.input(`r${rIndex}v${vIndex}`, value);
}
}

const query = `INSERT INTO [${table}] (${formatColumns(
columnString,
)}) VALUES ${valuesPlaceholder.join(', ')};`;

return request.query(query);
});
},
);
}

/**
* Extracts the WHERE condition from the items for DELETE
*
* @param {IDataObject[]} items The items to extract the values from
* @param {string} key The column name to extract the value from for the delete condition
*/
export function extractDeleteValues(items: IDataObject[], key: string): string {
return `(${items
.map((item) => (typeof item[key] === 'string' ? `'${item[key]}'` : item[key]))
.join(',')})`;
export async function updateOperation(tables: ITables, pool: mssql.ConnectionPool) {
return executeQueryQueue(
tables,
({ table, columnString, items }: OperationInputData): Array<Promise<object>> => {
return items.map(async (item) => {
const request = pool.request();
const columns = columnString.split(',').map((column) => column.trim());

const setValues: string[] = [];
const condition = `${item.updateKey} = @condition`;
request.input('condition', item[item.updateKey as string]);

for (const [index, col] of columns.entries()) {
setValues.push(`[${col}] = @v${index}`);
request.input(`v${index}`, item[col]);
}

const query = `UPDATE [${table}] SET ${setValues.join(', ')} WHERE ${condition};`;

return request.query(query);
});
},
);
}

export function formatColumns(columns: string) {
return columns
.split(',')
.map((column) => `"${column.trim()}"`)
.join(',');
export async function deleteOperation(tables: ITables, pool: mssql.ConnectionPool) {
const queriesResults = await Promise.all(
Object.keys(tables).map(async (table) => {
const deleteKeyResults = Object.keys(tables[table]).map(async (deleteKey) => {
const deleteItemsList = chunk(
tables[table][deleteKey].map((item) =>
copyInputItem(item as INodeExecutionData, [deleteKey]),
),
1000,
);
const queryQueue = deleteItemsList.map(async (deleteValues) => {
const request = pool.request();
const valuesPlaceholder: string[] = [];

for (const [index, entry] of deleteValues.entries()) {
valuesPlaceholder.push(`@v${index}`);
request.input(`v${index}`, entry[deleteKey]);
}

const query = `DELETE FROM [${table}] WHERE [${deleteKey}] IN (${valuesPlaceholder.join(
', ',
)});`;

return request.query(query);
});
return Promise.all(queryQueue);
});
return Promise.all(deleteKeyResults);
}),
);

return flatten(queriesResults).reduce(
(acc: number, resp: mssql.IResult<object>): number =>
(acc += resp.rowsAffected.reduce((sum, val) => (sum += val))),
0,
);
}
Loading
Loading