Skip to content

Commit

Permalink
fix(core): Force-upgrade http-cache-semantics to address CVE-2022-2…
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy committed Mar 21, 2023
1 parent f7401fb commit 2eda858
Show file tree
Hide file tree
Showing 23 changed files with 126 additions and 126 deletions.
24 changes: 0 additions & 24 deletions packages/cli/src/UserManagement/UserManagementHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,30 +204,6 @@ export async function getUserById(userId: string): Promise<User> {
return user;
}

/**
* Check if a URL contains an auth-excluded endpoint.
*/
export function isAuthExcluded(url: string, ignoredEndpoints: Readonly<string[]>): boolean {
return !!ignoredEndpoints
.filter(Boolean) // skip empty paths
.find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`));
}

/**
* Check if the endpoint is `POST /users/:id`.
*/
export function isPostUsersId(req: express.Request, restEndpoint: string): boolean {
return (
req.method === 'POST' &&
new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) &&
!req.url.includes('reinvite')
);
}

export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest {
return request.user !== undefined;
}

// ----------------------------------
// hashing
// ----------------------------------
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,10 @@ export class Start extends BaseCommand {
// eslint-disable-next-line no-restricted-syntax
for (const missingPackage of missingPackages) {
// eslint-disable-next-line no-await-in-loop
void (await this.loadNodesAndCredentials.loadNpmModule(
await this.loadNodesAndCredentials.loadNpmModule(
missingPackage.packageName,
missingPackage.version,
));
);
missingPackages.delete(missingPackage);
}
LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.');
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import validator from 'validator';
import { Get, Post, RestController } from '@/decorators';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
import { issueCookie, resolveJwt } from '@/auth/jwt';
Expand Down Expand Up @@ -56,7 +56,6 @@ export class AuthController {

/**
* Log in a user.
* Authless endpoint.
*/
@Post('/login')
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
Expand Down Expand Up @@ -140,7 +139,6 @@ export class AuthController {

/**
* Validate invite token to enable invitee to set up their account.
* Authless endpoint.
*/
@Get('/resolve-signup-token')
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
Expand Down Expand Up @@ -201,8 +199,8 @@ export class AuthController {

/**
* Log out a user.
* Authless endpoint.
*/
@Authorized()
@Post('/logout')
logout(req: Request, res: Response) {
res.clearCookie(AUTH_COOKIE_NAME);
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/controllers/ldap.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pick from 'lodash.pick';
import { Get, Post, Put, RestController } from '@/decorators';
import { Authorized, Get, Post, Put, RestController } from '@/decorators';
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
import { LdapService } from '@/Ldap/LdapService.ee';
import { LdapSync } from '@/Ldap/LdapSync.ee';
Expand All @@ -8,6 +8,7 @@ import { BadRequestError } from '@/ResponseHelper';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
import { InternalHooks } from '@/InternalHooks';

@Authorized(['global', 'owner'])
@RestController('/ldap')
export class LdapController {
constructor(
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/controllers/me.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import validator from 'validator';
import { plainToInstance } from 'class-transformer';
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
import {
compareHash,
hashPassword,
Expand All @@ -24,6 +24,7 @@ import type {
import { randomBytes } from 'crypto';
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';

@Authorized()
@RestController('/me')
export class MeController {
private readonly logger: ILogger;
Expand Down
12 changes: 2 additions & 10 deletions packages/cli/src/controllers/nodes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
STARTER_TEMPLATE_NAME,
UNKNOWN_FAILURE_REASON,
} from '@/constants';
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import { NodeRequest } from '@/requests';
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
import {
Expand All @@ -30,10 +30,10 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { InternalHooks } from '@/InternalHooks';
import { Push } from '@/push';
import { Config } from '@/config';
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';

const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;

@Authorized(['global', 'owner'])
@RestController('/nodes')
export class NodesController {
constructor(
Expand All @@ -43,14 +43,6 @@ export class NodesController {
private internalHooks: InternalHooks,
) {}

// TODO: move this into a new decorator `@Authorized`
@Middleware()
checkIfOwner(req: Request, res: Response, next: NextFunction) {
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner')
res.status(403).json({ status: 'error', message: 'Unauthorized' });
else next();
}

// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
@Middleware()
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/controllers/owner.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import validator from 'validator';
import { validateEntity } from '@/GenericHelpers';
import { Get, Post, RestController } from '@/decorators';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { BadRequestError } from '@/ResponseHelper';
import {
hashPassword,
Expand All @@ -18,6 +18,7 @@ import type { Settings } from '@db/entities/Settings';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';

@Authorized(['global', 'owner'])
@RestController('/owner')
export class OwnerController {
private readonly config: Config;
Expand Down
3 changes: 0 additions & 3 deletions packages/cli/src/controllers/passwordReset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export class PasswordResetController {

/**
* Send a password reset email.
* Authless endpoint.
*/
@Post('/forgot-password')
async forgotPassword(req: PasswordResetRequest.Email) {
Expand Down Expand Up @@ -161,7 +160,6 @@ export class PasswordResetController {

/**
* Verify password reset token and user ID.
* Authless endpoint.
*/
@Get('/resolve-password-token')
async resolvePasswordToken(req: PasswordResetRequest.Credentials) {
Expand Down Expand Up @@ -203,7 +201,6 @@ export class PasswordResetController {

/**
* Verify password reset token and user ID and update password.
* Authless endpoint.
*/
@Post('/change-password')
async changePassword(req: PasswordResetRequest.NewPassword, res: Response) {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/controllers/tags.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Request, Response, NextFunction } from 'express';
import { Request, NextFunction } from 'express';
import type { Repository } from 'typeorm';
import type { Config } from '@/config';
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces';
import { TagEntity } from '@db/entities/TagEntity';
import { getTagsWithCountDb } from '@/TagHelpers';
import { validateEntity } from '@/GenericHelpers';
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper';
import { TagsRequest } from '@/requests';

@Authorized()
@RestController('/tags')
export class TagsController {
private config: Config;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/controllers/translation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Request } from 'express';
import { ICredentialTypes } from 'n8n-workflow';
import { join } from 'path';
import { access } from 'fs/promises';
import { Get, RestController } from '@/decorators';
import { Authorized, Get, RestController } from '@/decorators';
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
import { Config } from '@/config';
import { NODES_BASE_DIR } from '@/constants';
Expand All @@ -14,6 +14,7 @@ export declare namespace TranslationRequest {
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
}

@Authorized()
@RestController('/')
export class TranslationController {
constructor(private config: Config, private credentialTypes: ICredentialTypes) {}
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/controllers/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { User } from '@db/entities/User';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { Delete, Get, Post, RestController } from '@/decorators';
import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
import {
addInviteLinkToUser,
generateUserInviteUrl,
Expand Down Expand Up @@ -35,7 +35,9 @@ import type {
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { PostHogClient } from '@/posthog';
import { NoAuthRequired } from '@/decorators/Authorized';

@Authorized(['global', 'owner'])
@RestController('/users')
export class UsersController {
private config: Config;
Expand Down Expand Up @@ -276,6 +278,7 @@ export class UsersController {
/**
* Fill out user shell with first name, last name, and password.
*/
@NoAuthRequired()
@Post('/:id')
async updateUser(req: UserRequest.Update, res: Response) {
const { id: inviteeId } = req.params;
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/databases/entities/Role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { idStringifier } from '../utils/transformers';

export type RoleNames = 'owner' | 'member' | 'user' | 'editor';
export type RoleScopes = 'global' | 'workflow' | 'credential';
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';

@Entity()
@Unique(['scope', 'name'])
Expand All @@ -32,4 +33,8 @@ export class Role extends AbstractEntity {

@OneToMany('SharedCredentials', 'role')
sharedCredentials: SharedCredentials[];

matches([scope, name]: [RoleScopes, RoleNames]) {
return this.name === name && this.scope === scope;
}
}
16 changes: 16 additions & 0 deletions packages/cli/src/decorators/Authorized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/naming-convention */
import { CONTROLLER_AUTH_ROLES } from './constants';
import type { AuthRoleMetadata } from './types';

export function Authorized(authRole: AuthRoleMetadata[string] = 'any'): Function {
return function (target: Function | Object, handlerName?: string) {
const controllerClass = handlerName ? target.constructor : target;
const authRoles = (Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) ??
{}) as AuthRoleMetadata;
authRoles[handlerName ?? '*'] = authRole;
Reflect.defineMetadata(CONTROLLER_AUTH_ROLES, authRoles, controllerClass);
};
}

export const NoAuthRequired = () => Authorized('none');
6 changes: 4 additions & 2 deletions packages/cli/src/decorators/RestController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { CONTROLLER_BASE_PATH } from './constants';
import type { RequestHandler } from 'express';
import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES } from './constants';

export const RestController =
(basePath: `/${string}` = '/'): ClassDecorator =>
(basePath: `/${string}` = '/', ...middlewares: RequestHandler[]): ClassDecorator =>
(target: object) => {
Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target);
Reflect.defineMetadata(CONTROLLER_MIDDLEWARES, middlewares, target);
};
1 change: 1 addition & 0 deletions packages/cli/src/decorators/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
1 change: 1 addition & 0 deletions packages/cli/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Authorized } from './Authorized';
export { RestController } from './RestController';
export { Get, Post, Put, Patch, Delete } from './Route';
export { Middleware } from './Middleware';
Expand Down
23 changes: 20 additions & 3 deletions packages/cli/src/decorators/registerController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Router } from 'express';
import type { Config } from '@/config';
import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants';
import {
CONTROLLER_AUTH_ROLES,
CONTROLLER_BASE_PATH,
CONTROLLER_MIDDLEWARES,
CONTROLLER_ROUTES,
} from './constants';
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
import type { Application, Request, Response, RequestHandler } from 'express';
import type { Controller, MiddlewareMetadata, RouteMetadata } from './types';
import type { AuthRoleMetadata, Controller, MiddlewareMetadata, RouteMetadata } from './types';
import { createAuthMiddleware } from '@/middlewares/createAuthMiddleware';

export const registerController = (app: Application, config: Config, controller: object) => {
const controllerClass = controller.constructor;
Expand All @@ -14,11 +20,20 @@ export const registerController = (app: Application, config: Config, controller:
if (!controllerBasePath)
throw new Error(`${controllerClass.name} is missing the RestController decorator`);

const middlewares = Reflect.getMetadata(
CONTROLLER_MIDDLEWARES,
controllerClass,
) as RequestHandler[];
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
| AuthRoleMetadata
| undefined;
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
if (routes.length > 0) {
const router = Router({ mergeParams: true });
const restBasePath = config.getEnv('endpoints.rest');
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/');
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`
.replace(/\/+/g, '/')
.replace(/\/$/, '');

const controllerMiddlewares = (
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
Expand All @@ -28,8 +43,10 @@ export const registerController = (app: Application, config: Config, controller:
);

routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => {
const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']);
router[method](
path,
...(authRole ? [createAuthMiddleware(authRole)] : []),
...controllerMiddlewares,
...routeMiddlewares,
send(async (req: Request, res: Response) =>
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/decorators/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { Request, Response, RequestHandler } from 'express';
import type { AuthRole } from '@db/entities/Role';

export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';

export type AuthRoleMetadata = Record<string, AuthRole>;

export interface MiddlewareMetadata {
handlerName: string;
}
Expand Down
Loading

0 comments on commit 2eda858

Please sign in to comment.