mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 09:27:50 +00:00
Implement preliminary pentest remediations
This commit is contained in:
@@ -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
|
||||||
@@ -116,6 +118,19 @@ export const inviteUserToOrganization = 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.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
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
@@ -154,6 +169,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,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,6 +22,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
|||||||
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) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ const userSchema = new Schema<IUser>(
|
|||||||
ip: String,
|
ip: String,
|
||||||
userAgent: String
|
userAgent: String
|
||||||
}],
|
}],
|
||||||
default: []
|
default: [],
|
||||||
|
select: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
export * from './user';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
export * from './bot';
|
export * from './bot';
|
||||||
export * from './integration';
|
export * from './integration';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user