fix: Upgrade axios to address CVE-2023-45857 (#7713)

[GH Advisory](https://github.com/advisories/GHSA-wf5p-g6vw-rhxx)
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-12-19 16:17:01 +01:00
committed by GitHub
parent 8e6b951a76
commit 64eb9bbc36
11 changed files with 104 additions and 69 deletions

View File

@@ -16,6 +16,7 @@ import type {
import { ClientOAuth2 } from '@n8n/client-oauth2';
import type {
AxiosError,
AxiosHeaders,
AxiosPromise,
AxiosProxyConfig,
AxiosRequestConfig,
@@ -186,23 +187,24 @@ const createFormDataObject = (data: Record<string, unknown>) => {
});
return formData;
};
function searchForHeader(headers: IDataObject, headerName: string) {
if (headers === undefined) {
function searchForHeader(config: AxiosRequestConfig, headerName: string) {
if (config.headers === undefined) {
return undefined;
}
const headerNames = Object.keys(headers);
const headerNames = Object.keys(config.headers);
headerName = headerName.toLowerCase();
return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName);
}
async function generateContentLengthHeader(formData: FormData, headers: IDataObject) {
if (!formData?.getLength) {
async function generateContentLengthHeader(config: AxiosRequestConfig) {
if (!(config.data instanceof FormData)) {
return;
}
try {
const length = await new Promise((res, rej) => {
formData.getLength((error: Error | null, length: number) => {
const length = await new Promise<number>((res, rej) => {
config.data.getLength((error: Error | null, length: number) => {
if (error) {
rej(error);
return;
@@ -210,9 +212,10 @@ async function generateContentLengthHeader(formData: FormData, headers: IDataObj
res(length);
});
});
headers = Object.assign(headers, {
config.headers = {
...config.headers,
'content-length': length,
});
};
} catch (error) {
Logger.error('Unable to calculate form data length', { error });
}
@@ -228,7 +231,7 @@ async function parseRequestObject(requestObject: IDataObject) {
const axiosConfig: AxiosRequestConfig = {};
if (requestObject.headers !== undefined) {
axiosConfig.headers = requestObject.headers as string;
axiosConfig.headers = requestObject.headers as AxiosHeaders;
}
// Let's start parsing the hardest part, which is the request body.
@@ -246,7 +249,7 @@ async function parseRequestObject(requestObject: IDataObject) {
);
const contentType =
contentTypeHeaderKeyName &&
(axiosConfig.headers[contentTypeHeaderKeyName] as string | undefined);
(axiosConfig.headers?.[contentTypeHeaderKeyName] as string | undefined);
if (contentType === 'application/x-www-form-urlencoded' && requestObject.formData === undefined) {
// there are nodes incorrectly created, informing the content type header
// and also using formData. Request lib takes precedence for the formData.
@@ -265,7 +268,7 @@ async function parseRequestObject(requestObject: IDataObject) {
axiosConfig.data = stringify(allData);
}
}
} else if (contentType && contentType.includes('multipart/form-data') !== false) {
} else if (contentType?.includes('multipart/form-data')) {
if (requestObject.formData !== undefined && requestObject.formData instanceof FormData) {
axiosConfig.data = requestObject.formData;
} else {
@@ -278,10 +281,10 @@ async function parseRequestObject(requestObject: IDataObject) {
}
// replace the existing header with a new one that
// contains the boundary property.
delete axiosConfig.headers[contentTypeHeaderKeyName];
delete axiosConfig.headers?.[contentTypeHeaderKeyName!];
const headers = axiosConfig.data.getHeaders();
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
await generateContentLengthHeader(axiosConfig);
} else {
// When using the `form` property it means the content should be x-www-form-urlencoded.
if (requestObject.form !== undefined && requestObject.body === undefined) {
@@ -291,7 +294,7 @@ async function parseRequestObject(requestObject: IDataObject) {
? stringify(requestObject.form, { format: 'RFC3986' })
: stringify(requestObject.form).toString();
if (axiosConfig.headers !== undefined) {
const headerName = searchForHeader(axiosConfig.headers, 'content-type');
const headerName = searchForHeader(axiosConfig, 'content-type');
if (headerName) {
delete axiosConfig.headers[headerName];
}
@@ -305,9 +308,11 @@ async function parseRequestObject(requestObject: IDataObject) {
// remove any "content-type" that might exist.
if (axiosConfig.headers !== undefined) {
const headers = Object.keys(axiosConfig.headers);
headers.forEach((header) =>
header.toLowerCase() === 'content-type' ? delete axiosConfig.headers[header] : null,
);
headers.forEach((header) => {
if (header.toLowerCase() === 'content-type') {
delete axiosConfig.headers?.[header];
}
});
}
if (requestObject.formData instanceof FormData) {
@@ -318,7 +323,7 @@ async function parseRequestObject(requestObject: IDataObject) {
// Mix in headers as FormData creates the boundary.
const headers = axiosConfig.data.getHeaders();
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
await generateContentLengthHeader(axiosConfig);
} else if (requestObject.body !== undefined) {
// If we have body and possibly form
if (requestObject.form !== undefined && requestObject.body) {
@@ -755,7 +760,7 @@ export async function proxyRequestToAxios(
return configObject.resolveWithFullResponse
? {
body,
headers: response.headers,
headers: { ...response.headers },
statusCode: response.status,
statusMessage: response.statusText,
request: response.request,
@@ -852,7 +857,7 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
const { body } = n8nRequest;
if (body) {
// Let's add some useful header standards here.
const existingContentTypeHeaderKey = searchForHeader(axiosRequest.headers, 'content-type');
const existingContentTypeHeaderKey = searchForHeader(axiosRequest, 'content-type');
if (existingContentTypeHeaderKey === undefined) {
axiosRequest.headers = axiosRequest.headers || {};
// We are only setting content type headers if the user did
@@ -866,7 +871,7 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
axiosRequest.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
} else if (
axiosRequest.headers[existingContentTypeHeaderKey] === 'application/x-www-form-urlencoded'
axiosRequest.headers?.[existingContentTypeHeaderKey] === 'application/x-www-form-urlencoded'
) {
axiosRequest.data = new URLSearchParams(n8nRequest.body as Record<string, string>);
}
@@ -879,19 +884,25 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
}
if (n8nRequest.json) {
const key = searchForHeader(axiosRequest.headers, 'accept');
const key = searchForHeader(axiosRequest, 'accept');
// If key exists, then the user has set both accept
// header and the json flag. Header should take precedence.
if (!key) {
axiosRequest.headers.Accept = 'application/json';
axiosRequest.headers = {
...axiosRequest.headers,
Accept: 'application/json',
};
}
}
const userAgentHeader = searchForHeader(axiosRequest.headers, 'user-agent');
const userAgentHeader = searchForHeader(axiosRequest, 'user-agent');
// If key exists, then the user has set both accept
// header and the json flag. Header should take precedence.
if (!userAgentHeader) {
axiosRequest.headers['User-Agent'] = 'n8n';
axiosRequest.headers = {
...axiosRequest.headers,
'User-Agent': 'n8n',
};
}
if (n8nRequest.ignoreHttpStatusErrors) {

View File

@@ -7,17 +7,18 @@ import { sign } from 'aws4';
import { isStream, parseXml, writeBlockedMessage } from './utils';
import { ApplicationError, LoggerProxy as Logger } from 'n8n-workflow';
import type { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios';
import type { Request as Aws4Options, Credentials as Aws4Credentials } from 'aws4';
import type {
Bucket,
ConfigSchemaCredentials,
ListPage,
MetadataResponseHeaders,
RawListPage,
RequestOptions,
} from './types';
import type { Readable } from 'stream';
import type { BinaryData } from '..';
import type { BinaryData } from '../BinaryData/types';
@Service()
export class ObjectStoreService {
@@ -115,19 +116,11 @@ export class ObjectStoreService {
* @doc https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
*/
async getMetadata(fileId: string) {
type Response = {
headers: {
'content-length': string;
'content-type'?: string;
'x-amz-meta-filename'?: string;
} & BinaryData.PreWriteMetadata;
};
const path = `${this.bucket.name}/${fileId}`;
const response: Response = await this.request('HEAD', this.host, path);
const response = await this.request('HEAD', this.host, path);
return response.headers;
return response.headers as MetadataResponseHeaders;
}
/**
@@ -239,10 +232,16 @@ export class ObjectStoreService {
this.logger.warn(logMessage);
return { status: 403, statusText: 'Forbidden', data: logMessage, headers: {}, config: {} };
return {
status: 403,
statusText: 'Forbidden',
data: logMessage,
headers: {},
config: {} as InternalAxiosRequestConfig,
};
}
private async request(
private async request<T>(
method: Method,
host: string,
rawPath = '',
@@ -275,7 +274,7 @@ export class ObjectStoreService {
try {
this.logger.debug('Sending request to S3', { config });
return await axios.request<unknown>(config);
return await axios.request<T>(config);
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);

View File

@@ -1,4 +1,5 @@
import type { ResponseType } from 'axios';
import type { AxiosResponseHeaders, ResponseType } from 'axios';
import type { BinaryData } from '../BinaryData/types';
export type RawListPage = {
listBucketResult: {
@@ -31,4 +32,10 @@ export type RequestOptions = {
responseType?: ResponseType;
};
export type MetadataResponseHeaders = AxiosResponseHeaders & {
'content-length': string;
'content-type'?: string;
'x-amz-meta-filename'?: string;
} & BinaryData.PreWriteMetadata;
export type ConfigSchemaCredentials = { accessKey: string; accessSecret: string };