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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 104 additions and 69 deletions

View file

@ -20,6 +20,6 @@
"dist/**/*" "dist/**/*"
], ],
"dependencies": { "dependencies": {
"axios": "0.21.4" "axios": "1.6.2"
} }
} }

View file

@ -108,7 +108,7 @@
"@rudderstack/rudder-sdk-node": "1.0.6", "@rudderstack/rudder-sdk-node": "1.0.6",
"@sentry/integrations": "7.87.0", "@sentry/integrations": "7.87.0",
"@sentry/node": "7.87.0", "@sentry/node": "7.87.0",
"axios": "0.21.4", "axios": "1.6.2",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bull": "4.10.2", "bull": "4.10.2",

View file

@ -251,19 +251,15 @@ export class VaultProvider extends SecretsProvider {
this.#http = axios.create({ baseURL: baseURL.toString() }); this.#http = axios.create({ baseURL: baseURL.toString() });
if (this.settings.namespace) { if (this.settings.namespace) {
this.#http.interceptors.request.use((config) => { this.#http.interceptors.request.use((config) => {
return { config.headers['X-Vault-Namespace'] = this.settings.namespace;
...config, return config;
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-assignment
headers: { ...config.headers, 'X-Vault-Namespace': this.settings.namespace },
};
}); });
} }
this.#http.interceptors.request.use((config) => { this.#http.interceptors.request.use((config) => {
if (!this.#currentToken) { if (this.#currentToken) {
return config; config.headers['X-Vault-Token'] = this.#currentToken;
} }
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-assignment return config;
return { ...config, headers: { ...config.headers, 'X-Vault-Token': this.#currentToken } };
}); });
} }

View file

@ -324,9 +324,15 @@ export class MessageEventBusDestinationWebhook
password: httpBasicAuth.password as string, password: httpBasicAuth.password as string,
}; };
} else if (httpHeaderAuth) { } else if (httpHeaderAuth) {
this.axiosRequestOptions.headers[httpHeaderAuth.name as string] = httpHeaderAuth.value; this.axiosRequestOptions.headers = {
...this.axiosRequestOptions.headers,
[httpHeaderAuth.name as string]: httpHeaderAuth.value as string,
};
} else if (httpQueryAuth) { } else if (httpQueryAuth) {
this.axiosRequestOptions.params[httpQueryAuth.name as string] = httpQueryAuth.value; this.axiosRequestOptions.params = {
...this.axiosRequestOptions.params,
[httpQueryAuth.name as string]: httpQueryAuth.value as string,
};
} else if (httpDigestAuth) { } else if (httpDigestAuth) {
this.axiosRequestOptions.auth = { this.axiosRequestOptions.auth = {
username: httpDigestAuth.user as string, username: httpDigestAuth.user as string,

View file

@ -50,7 +50,7 @@
"dependencies": { "dependencies": {
"@n8n/client-oauth2": "workspace:*", "@n8n/client-oauth2": "workspace:*",
"aws4": "1.11.0", "aws4": "1.11.0",
"axios": "0.21.4", "axios": "1.6.2",
"concat-stream": "2.0.0", "concat-stream": "2.0.0",
"cron": "1.7.2", "cron": "1.7.2",
"fast-glob": "3.2.12", "fast-glob": "3.2.12",

View file

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

View file

@ -7,17 +7,18 @@ import { sign } from 'aws4';
import { isStream, parseXml, writeBlockedMessage } from './utils'; import { isStream, parseXml, writeBlockedMessage } from './utils';
import { ApplicationError, LoggerProxy as Logger } from 'n8n-workflow'; 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 { Request as Aws4Options, Credentials as Aws4Credentials } from 'aws4';
import type { import type {
Bucket, Bucket,
ConfigSchemaCredentials, ConfigSchemaCredentials,
ListPage, ListPage,
MetadataResponseHeaders,
RawListPage, RawListPage,
RequestOptions, RequestOptions,
} from './types'; } from './types';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import type { BinaryData } from '..'; import type { BinaryData } from '../BinaryData/types';
@Service() @Service()
export class ObjectStoreService { export class ObjectStoreService {
@ -115,19 +116,11 @@ export class ObjectStoreService {
* @doc https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html * @doc https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
*/ */
async getMetadata(fileId: string) { 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 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); 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, method: Method,
host: string, host: string,
rawPath = '', rawPath = '',
@ -275,7 +274,7 @@ export class ObjectStoreService {
try { try {
this.logger.debug('Sending request to S3', { config }); this.logger.debug('Sending request to S3', { config });
return await axios.request<unknown>(config); return await axios.request<T>(config);
} catch (e) { } catch (e) {
const error = e instanceof Error ? e : new Error(`${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 = { export type RawListPage = {
listBucketResult: { listBucketResult: {
@ -31,4 +32,10 @@ export type RequestOptions = {
responseType?: ResponseType; 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 }; export type ConfigSchemaCredentials = { accessKey: string; accessSecret: string };

View file

@ -50,7 +50,7 @@
"@n8n/permissions": "workspace:*", "@n8n/permissions": "workspace:*",
"@vueuse/components": "^10.5.0", "@vueuse/components": "^10.5.0",
"@vueuse/core": "^10.5.0", "@vueuse/core": "^10.5.0",
"axios": "^0.21.1", "axios": "^1.6.2",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"codemirror-lang-html-n8n": "^1.0.0", "codemirror-lang-html-n8n": "^1.0.0",
"codemirror-lang-n8n-expression": "^0.2.0", "codemirror-lang-n8n-expression": "^0.2.0",

View file

@ -43,6 +43,21 @@ class ResponseError extends Error {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyParamSerializer = (params: Record<string, any>) =>
Object.keys(params)
.filter((key) => params[key] !== undefined)
.map((key) => {
if (Array.isArray(params[key])) {
return params[key].map((v) => `${key}[]=${encodeURIComponent(v)}`).join('&');
}
if (typeof params[key] === 'object') {
params[key] = JSON.stringify(params[key]);
}
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');
export async function request(config: { export async function request(config: {
method: Method; method: Method;
baseURL: string; baseURL: string;
@ -67,8 +82,9 @@ export async function request(config: {
} }
if (['POST', 'PATCH', 'PUT'].includes(method)) { if (['POST', 'PATCH', 'PUT'].includes(method)) {
options.data = data; options.data = data;
} else { } else if (data) {
options.params = data; options.params = data;
options.paramsSerializer = legacyParamSerializer;
} }
try { try {

View file

@ -259,8 +259,8 @@ importers:
packages/@n8n/client-oauth2: packages/@n8n/client-oauth2:
dependencies: dependencies:
axios: axios:
specifier: 0.21.4 specifier: 1.6.2
version: 0.21.4 version: 1.6.2
packages/@n8n/nodes-langchain: packages/@n8n/nodes-langchain:
dependencies: dependencies:
@ -460,8 +460,8 @@ importers:
specifier: 7.87.0 specifier: 7.87.0
version: 7.87.0 version: 7.87.0
axios: axios:
specifier: 0.21.4 specifier: 1.6.2
version: 0.21.4 version: 1.6.2
basic-auth: basic-auth:
specifier: 2.0.1 specifier: 2.0.1
version: 2.0.1 version: 2.0.1
@ -818,8 +818,8 @@ importers:
specifier: 1.11.0 specifier: 1.11.0
version: 1.11.0 version: 1.11.0
axios: axios:
specifier: 0.21.4 specifier: 1.6.2
version: 0.21.4 version: 1.6.2
concat-stream: concat-stream:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
@ -1093,8 +1093,8 @@ importers:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(vue@3.3.4) version: 10.5.0(vue@3.3.4)
axios: axios:
specifier: ^0.21.1 specifier: ^1.6.2
version: 0.21.4 version: 1.6.2
chart.js: chart.js:
specifier: ^4.4.0 specifier: ^4.4.0
version: 4.4.0 version: 4.4.0
@ -11389,8 +11389,8 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
/axios@1.4.0: /axios@1.6.2:
resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==}
dependencies: dependencies:
follow-redirects: 1.15.2(debug@4.3.4) follow-redirects: 1.15.2(debug@4.3.4)
form-data: 4.0.0 form-data: 4.0.0
@ -16246,7 +16246,7 @@ packages:
/infisical-node@1.3.0: /infisical-node@1.3.0:
resolution: {integrity: sha512-tTnnExRAO/ZyqiRdnSlBisErNToYWgtunMWh+8opClEt5qjX7l6HC/b4oGo2AuR2Pf41IR+oqo+dzkM1TCvlUA==} resolution: {integrity: sha512-tTnnExRAO/ZyqiRdnSlBisErNToYWgtunMWh+8opClEt5qjX7l6HC/b4oGo2AuR2Pf41IR+oqo+dzkM1TCvlUA==}
dependencies: dependencies:
axios: 1.4.0 axios: 1.6.2
dotenv: 16.3.1 dotenv: 16.3.1
tweetnacl: 1.0.3 tweetnacl: 1.0.3
tweetnacl-util: 0.15.1 tweetnacl-util: 0.15.1