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
+1 -1
View File
@@ -89,7 +89,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body; const { email, clientProof } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email 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'); if (!user) throw new Error('Failed to find user');
@@ -7,8 +7,10 @@ import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -115,6 +117,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); 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({ invitee = await User.findOne({
email: inviteeEmail email: inviteeEmail
@@ -153,6 +168,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
// case: invitee has never been invited before // case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({ await new MembershipOrg({
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
@@ -28,7 +28,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ 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({ 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"
}); });
} }
@@ -8,6 +8,7 @@ import {
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; 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 * 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; let email: string;
try { try {
email = req.body.email; email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
@@ -14,6 +14,7 @@ import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork, deleteWorkspace as deleteWork,
} from "../../helpers/workspace"; } from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
@@ -141,6 +142,7 @@ export const getWorkspace = async (req: Request, res: Response) => {
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; let workspace;
try { try {
const { workspaceName, organizationId } = req.body; 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"); 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) { if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long"); throw new Error("Workspace names must be at least 1-character long");
} }
+7 -1
View File
@@ -22,6 +22,10 @@ import {
getHttpsEnabled getHttpsEnabled
} from '../../config'; } from '../../config';
// note: move this out
import path from 'path';
import fs from 'fs';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
@@ -93,7 +97,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body; const { email, clientProof } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email 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'); 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] * Send MFA token to email [email]
* @param req * @param req
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -34,12 +34,13 @@ const sendMail = async ({
const temp = handlebars.compile(html); const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions); const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({ const x = await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
to: recipients.join(', '), to: recipients.join(', '),
subject: subjectLine, subject: subjectLine,
html: htmlToSend html: htmlToSend
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
+2 -11
View File
@@ -8,9 +8,6 @@ const apiLimiter = rateLimit({
legacyHeaders: false, legacyHeaders: false,
skip: (request) => { skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status' return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
} }
}); });
@@ -19,10 +16,7 @@ const authLimit = rateLimit({
windowMs: 60 * 1000, windowMs: 60 * 1000,
max: 10, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false
keyGenerator: (req, res) => {
return req.clientIp
}
}); });
// 10 requests per hour // 10 requests per hour
@@ -30,10 +24,7 @@ const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 10, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false
keyGenerator: (req, res) => {
return req.clientIp
}
}); });
const authLimiter = (req: any, res: any, next: any) => { const authLimiter = (req: any, res: any, next: any) => {
-3
View File
@@ -13,7 +13,6 @@ import swaggerUi = require('swagger-ui-express');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require('../spec.json'); const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
import { apiLimiter } from './helpers/rateLimiter'; import { apiLimiter } from './helpers/rateLimiter';
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
@@ -88,8 +87,6 @@ const main = async () => {
}) })
); );
app.use(requestIp.mw());
if ((await getNodeEnv()) === 'production') { if ((await getNodeEnv()) === 'production') {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
+2 -1
View File
@@ -94,7 +94,8 @@ const userSchema = new Schema<IUser>(
ip: String, ip: String,
userAgent: String userAgent: String
}], }],
default: [] default: [],
select: false
} }
}, },
{ {
+1 -1
View File
@@ -26,7 +26,7 @@ router.post(
router.post( router.post(
'/mfa/send', '/mfa/send',
authLimiter, authLimiter,
body('email').isString().trim().notEmpty(), body('email').isString().trim().notEmpty().isEmail(),
validateRequest, validateRequest,
authController.sendMfaToken authController.sendMfaToken
); );
+1
View File
@@ -1,3 +1,4 @@
export * from './user';
export * from './workspace'; export * from './workspace';
export * from './bot'; export * from './bot';
export * from './integration'; export * from './integration';
+19 -1
View File
@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
IUser, IUser,
@@ -8,7 +10,7 @@ import {
} from '../models'; } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import _ from 'lodash'; import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { import {
validateMembershipOrg validateMembershipOrg
} from '../helpers/membershipOrg'; } from '../helpers/membershipOrg';
@@ -17,6 +19,22 @@ import {
PERMISSION_WRITE_SECRETS PERMISSION_WRITE_SECRETS
} from '../variables'; } 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 * Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions * with id [workspaceId] and its environment [environment] with required permissions