Implement preliminary pentest remediations

This commit is contained in:
Tuan Dang
2023-06-05 00:44:10 +01:00
parent 4865b69e6d
commit 0c18bd71c4
14 changed files with 3592 additions and 22 deletions

View File

@@ -89,7 +89,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');

View File

@@ -7,8 +7,10 @@ import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Delete organization membership with id [membershipOrgId] from organization
@@ -115,6 +117,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
const plan = await EELicenseService.getOrganizationPlan(organizationId);
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
});
}
}
invitee = await User.findOne({
email: inviteeEmail
@@ -153,6 +168,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,

View File

@@ -28,7 +28,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
// case: user has already completed account
return res.status(403).send({
error: 'Failed to send email verification for password reset'
message: "If an account exists with this email, a password reset link has been sent"
});
}
@@ -56,7 +56,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
}
return res.status(200).send({
message: `Sent an email for account recovery to ${email}`
message:"If an account exists with this email, a password reset link has been sent"
});
}

View File

@@ -8,6 +8,7 @@ import {
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -20,6 +21,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string;
try {
email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {

View File

@@ -14,6 +14,7 @@ import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
@@ -141,6 +142,7 @@ export const getWorkspace = async (req: Request, res: Response) => {
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
@@ -154,6 +156,18 @@ export const createWorkspace = async (req: Request, res: Response) => {
throw new Error("Failed to validate organization membership");
}
const plan = await EELicenseService.getOrganizationPlan(organizationId);
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}

View File

@@ -22,6 +22,10 @@ import {
getHttpsEnabled
} from '../../config';
// note: move this out
import path from 'path';
import fs from 'fs';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
@@ -93,7 +97,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
@@ -231,6 +235,8 @@ export const login2 = async (req: Request, res: Response) => {
}
};
import { validateUserEmail } from '../../validation';
/**
* Send MFA token to email [email]
* @param req

File diff suppressed because it is too large Load Diff

View File

@@ -34,12 +34,13 @@ const sendMail = async ({
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({
const x = await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);

View File

@@ -8,9 +8,6 @@ const apiLimiter = rateLimit({
legacyHeaders: false,
skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
}
});
@@ -19,10 +16,7 @@ const authLimit = rateLimit({
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
}
legacyHeaders: false
});
// 10 requests per hour
@@ -30,10 +24,7 @@ const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
}
legacyHeaders: false
});
const authLimiter = (req: any, res: any, next: any) => {

View File

@@ -13,7 +13,6 @@ import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
@@ -88,8 +87,6 @@ const main = async () => {
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security
// in production

View File

@@ -94,7 +94,8 @@ const userSchema = new Schema<IUser>(
ip: String,
userAgent: String
}],
default: []
default: [],
select: false
}
},
{

View File

@@ -26,7 +26,7 @@ router.post(
router.post(
'/mfa/send',
authLimiter,
body('email').isString().trim().notEmpty(),
body('email').isString().trim().notEmpty().isEmail(),
validateRequest,
authController.sendMfaToken
);

View File

@@ -1,3 +1,4 @@
export * from './user';
export * from './workspace';
export * from './bot';
export * from './integration';

View File

@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { Types } from 'mongoose';
import {
IUser,
@@ -8,7 +10,7 @@ import {
} from '../models';
import { validateMembership } from '../helpers/membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
@@ -17,6 +19,22 @@ import {
PERMISSION_WRITE_SECRETS
} from '../variables';
/**
* Validate that email [email] is not disposable
* @param email - email to validate
*/
export const validateUserEmail = (email: string) => {
const emailDomain = email.split('@')[1];
const disposableEmails = fs.readFileSync(
path.resolve(__dirname, '../data/' + 'disposable_emails.txt'),
'utf8'
).split('\n');
if (disposableEmails.includes(emailDomain)) throw ValidationError({
message: 'Failed to validate email as non-disposable'
});
}
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions