fix: Upgrade sse-channel to mitigate CVE-2019-10744 (#4835)

sse-channel 4 removed CORS support, that's why we need to handle CORS for `/push` ourselves now.
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2022-12-07 15:13:36 +01:00
committed by GitHub
parent 1fc17b5d81
commit 7e1a13f9b2
7 changed files with 71 additions and 139 deletions

View File

@@ -1,48 +1,20 @@
// @ts-ignore
import sseChannel from 'sse-channel';
import express from 'express';
import SSEChannel from 'sse-channel';
import type { Request, Response } from 'express';
import { LoggerProxy as Logger } from 'n8n-workflow';
import type { IPushData, IPushDataType } from '@/Interfaces';
interface SSEChannelOptions {
cors?: {
origins: string[];
};
}
namespace SSE {
export type Channel = {
on(event: string, handler: (channel: string, res: express.Response) => void): void;
removeClient: (res: express.Response) => void;
addClient: (req: express.Request, res: express.Response) => void;
send: (msg: string, clients?: express.Response[]) => void;
};
}
export class Push {
private channel: SSE.Channel;
private channel = new SSEChannel();
private connections: {
[key: string]: express.Response;
} = {};
private connections: Record<string, Response> = {};
constructor() {
const options: SSEChannelOptions = {};
if (process.env.NODE_ENV !== 'production') {
options.cors = {
// Allow access also from frontend when developing
origins: ['http://localhost:8080'],
};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
this.channel = new sseChannel(options) as SSE.Channel;
this.channel.on('disconnect', (channel: string, res: express.Response) => {
this.channel.on('disconnect', (channel: string, res: Response) => {
if (res.req !== undefined) {
Logger.debug(`Remove editor-UI session`, { sessionId: res.req.query.sessionId });
delete this.connections[res.req.query.sessionId as string];
const { sessionId } = res.req.query;
Logger.debug(`Remove editor-UI session`, { sessionId });
delete this.connections[sessionId as string];
}
});
}
@@ -51,10 +23,10 @@ export class Push {
* Adds a new push connection
*
* @param {string} sessionId The id of the session
* @param {express.Request} req The request
* @param {express.Response} res The response
* @param {Request} req The request
* @param {Response} res The response
*/
add(sessionId: string, req: express.Request, res: express.Response) {
add(sessionId: string, req: Request, res: Response) {
Logger.debug(`Add editor-UI session`, { sessionId });
if (this.connections[sessionId] !== undefined) {

View File

@@ -158,6 +158,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import { toHttpNodeParameters } from '@/CurlConverterHelper';
import { setupErrorMiddleware } from '@/ErrorReporting';
import { getLicense } from '@/License';
import { corsMiddleware } from './middlewares/cors';
require('body-parser-xml')(bodyParser);
@@ -624,30 +625,25 @@ class App {
this.app.use(cookieParser());
// Get push connections
this.app.use(
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.url.indexOf(`/${this.restEndpoint}/push`) === 0) {
if (req.query.sessionId === undefined) {
next(new Error('The query parameter "sessionId" is missing!'));
return;
}
this.app.use(`/${this.restEndpoint}/push`, corsMiddleware, async (req, res, next) => {
const { sessionId } = req.query;
if (sessionId === undefined) {
next(new Error('The query parameter "sessionId" is missing!'));
return;
}
if (isUserManagementEnabled()) {
try {
const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
res.status(401).send('Unauthorized');
return;
}
}
this.push.add(req.query.sessionId as string, req, res);
if (isUserManagementEnabled()) {
try {
const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
res.status(401).send('Unauthorized');
return;
}
next();
},
);
}
this.push.add(sessionId as string, req, res);
});
// Compress the response data
this.app.use(compression());
@@ -719,19 +715,7 @@ class App {
}),
);
if (process.env.NODE_ENV !== 'production') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
next();
});
}
this.app.use(corsMiddleware);
// eslint-disable-next-line consistent-return
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {

View File

@@ -27,6 +27,7 @@ import type { ICustomRequest, IExternalHooksClass, IPackageVersions } from '@/In
import config from '@/config';
import { WEBHOOK_METHODS } from '@/WebhookHelpers';
import { setupErrorMiddleware } from '@/ErrorReporting';
import { corsMiddleware } from './middlewares/cors';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser);
@@ -278,18 +279,7 @@ class App {
}),
);
if (process.env.NODE_ENV !== 'production') {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
// Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
next();
});
}
this.app.use(corsMiddleware);
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (!Db.isInitialized) {

View File

@@ -0,0 +1,18 @@
import type { RequestHandler } from 'express';
const { NODE_ENV } = process.env;
const inDevelopment = !NODE_ENV || NODE_ENV === 'development';
export const corsMiddleware: RequestHandler = (req, res, next) => {
if (inDevelopment && 'origin' in req.headers) {
// Allow access also from frontend when developing
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, sessionid',
);
}
next();
};

17
packages/cli/src/sse-channel.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import type { Request, Response } from 'express';
declare module 'sse-channel' {
declare class Channel {
constructor();
on(event: string, handler: (channel: string, res: Response) => void): void;
removeClient: (res: Response) => void;
addClient: (req: Request, res: Response) => void;
send: (msg: string, clients?: Response[]) => void;
}
export = Channel;
}