Skip to content

Commit

Permalink
fix(HTTP Request Node): Sanitize secrets of predefined credentials (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
elsmr authored Jun 5, 2024
1 parent 5361e9f commit 84f091d
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 67 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2844,7 +2844,8 @@ const getCommonWorkflowFunctions = (
getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
getInstanceId: () => Container.get(InstanceSettings).instanceId,
getTimezone: () => getTimezone(workflow),

getCredentialsProperties: (type: string) =>
additionalData.credentialsHelper.getCredentialsProperties(type),
prepareOutputData: async (outputData) => [outputData],
});

Expand Down
62 changes: 58 additions & 4 deletions packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import type { SecureContextOptions } from 'tls';
import type {
ICredentialDataDecryptedObject,
IDataObject,
INodeExecutionData,
INodeProperties,
IOAuth2Options,
IRequestOptions,
} from 'n8n-workflow';

import set from 'lodash/set';
import isPlainObject from 'lodash/isPlainObject';

import FormData from 'form-data';
import type { HttpSslAuthCredentials } from './interfaces';
import { formatPrivateKey } from '../../utils/utilities';
import type { HttpSslAuthCredentials } from './interfaces';
import get from 'lodash/get';

export type BodyParameter = {
name: string;
Expand All @@ -29,7 +33,33 @@ export const replaceNullValues = (item: INodeExecutionData) => {
return item;
};

export function sanitizeUiMessage(request: IRequestOptions, authDataKeys: IAuthDataSanitizeKeys) {
export const REDACTED = '**hidden**';

function isObject(obj: unknown): obj is IDataObject {
return isPlainObject(obj);
}

function redact<T = unknown>(obj: T, secrets: string[]): T {
if (typeof obj === 'string') {
return secrets.reduce((safe, secret) => safe.replace(secret, REDACTED), obj) as T;
}

if (Array.isArray(obj)) {
return obj.map((item) => redact(item, secrets)) as T;
} else if (isObject(obj)) {
for (const [key, value] of Object.entries(obj)) {
(obj as IDataObject)[key] = redact(value, secrets);
}
}

return obj;
}

export function sanitizeUiMessage(
request: IRequestOptions,
authDataKeys: IAuthDataSanitizeKeys,
secrets?: string[],
) {
let sendRequest = request as unknown as IDataObject;

// Protect browser from sending large binary data
Expand All @@ -38,7 +68,7 @@ export function sanitizeUiMessage(request: IRequestOptions, authDataKeys: IAuthD
...request,
body: `Binary data got replaced with this text. Original was a Buffer with a size of ${
(request.body as string).length
} byte.`,
} bytes.`,
};
}

Expand All @@ -50,7 +80,7 @@ export function sanitizeUiMessage(request: IRequestOptions, authDataKeys: IAuthD
// eslint-disable-next-line @typescript-eslint/no-loop-func
(acc: IDataObject, curr) => {
acc[curr] = authDataKeys[requestProperty].includes(curr)
? '** hidden **'
? REDACTED
: (sendRequest[requestProperty] as IDataObject)[curr];
return acc;
},
Expand All @@ -59,9 +89,33 @@ export function sanitizeUiMessage(request: IRequestOptions, authDataKeys: IAuthD
};
}

if (secrets && secrets.length > 0) {
return redact(sendRequest, secrets);
}

return sendRequest;
}

export function getSecrets(
properties: INodeProperties[],
credentials: ICredentialDataDecryptedObject,
): string[] {
const sensitivePropNames = new Set(
properties.filter((prop) => prop.typeOptions?.password).map((prop) => prop.name),
);

const secrets = Object.entries(credentials)
.filter(([propName]) => sensitivePropNames.has(propName))
.map(([_, value]) => value)
.filter((value): value is string => typeof value === 'string');
const oauthAccessToken = get(credentials, 'oauthTokenData.access_token');
if (typeof oauthAccessToken === 'string') {
secrets.push(oauthAccessToken);
}

return secrets;
}

export const getOAuth2AdditionalParameters = (nodeCredentialType: string) => {
const oAuth2Options: { [credentialType: string]: IOAuth2Options } = {
bitlyOAuth2Api: {
Expand Down
46 changes: 37 additions & 9 deletions packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions';
import {
binaryContentTypes,
getOAuth2AdditionalParameters,
getSecrets,
prepareRequestBody,
reduceAsync,
replaceNullValues,
sanitizeUiMessage,
setAgentOptions,
} from '../GenericFunctions';
import { keysToLowercase } from '@utils/utilities';
import type { HttpSslAuthCredentials } from '../interfaces';
import { keysToLowercase } from '@utils/utilities';

function toText<T>(data: T) {
if (typeof data === 'object' && data !== null) {
Expand Down Expand Up @@ -1299,7 +1300,12 @@ export class HttpRequestV3 implements INodeType {
requestInterval: number;
};

const sanitazedRequests: IDataObject[] = [];
const requests: Array<{
options: IRequestOptions;
authKeys: IAuthDataSanitizeKeys;
credentialType?: string;
}> = [];

for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (authentication === 'genericCredentialType') {
genericCredentialType = this.getNodeParameter('genericAuthType', 0) as string;
Expand Down Expand Up @@ -1696,11 +1702,11 @@ export class HttpRequestV3 implements INodeType {
}
}

try {
const sanitazedRequestOptions = sanitizeUiMessage(requestOptions, authDataKeys);
this.sendMessageToUI(sanitazedRequestOptions);
sanitazedRequests.push(sanitazedRequestOptions);
} catch (e) {}
requests.push({
options: requestOptions,
authKeys: authDataKeys,
credentialType: nodeCredentialType,
});

if (pagination && pagination.paginationMode !== 'off') {
let continueExpression = '={{false}}';
Expand Down Expand Up @@ -1827,7 +1833,29 @@ export class HttpRequestV3 implements INodeType {
requestPromises.push(requestWithAuthentication);
}
}
const promisesResponses = await Promise.allSettled(requestPromises);

const sanitizedRequests: IDataObject[] = [];
const promisesResponses = await Promise.allSettled(
requestPromises.map(
async (requestPromise, itemIndex) =>
await requestPromise.finally(async () => {
try {
// Secrets need to be read after the request because secrets could have changed
// For example: OAuth token refresh, preAuthentication
const { options, authKeys, credentialType } = requests[itemIndex];
let secrets: string[] = [];
if (credentialType) {
const properties = this.getCredentialsProperties(credentialType);
const credentials = await this.getCredentials(credentialType, itemIndex);
secrets = getSecrets(properties, credentials);
}
const sanitizedRequestOptions = sanitizeUiMessage(options, authKeys, secrets);
sanitizedRequests.push(sanitizedRequestOptions);
this.sendMessageToUI(sanitizedRequestOptions);
} catch (e) {}
}),
),
);

let responseData: any;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
Expand All @@ -1842,7 +1870,7 @@ export class HttpRequestV3 implements INodeType {
responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString();
}
const error = new NodeApiError(this.getNode(), responseData as JsonObject, { itemIndex });
set(error, 'context.request', sanitazedRequests[itemIndex]);
set(error, 'context.request', sanitizedRequests[itemIndex]);
throw error;
} else {
removeCircularRefs(responseData.reason as JsonObject);
Expand Down
Loading

0 comments on commit 84f091d

Please sign in to comment.