improvement: add more aggresive rate limiting on smtp endpoints

This commit is contained in:
Scott Wilson
2025-06-04 13:27:08 -07:00
parent 83c53b9d5a
commit 698260cba6
10 changed files with 54 additions and 22 deletions

View File

@@ -11,7 +11,7 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
return { return {
errorResponseBuilder: (_, context) => { errorResponseBuilder: (_, context) => {
throw new RateLimitError({ throw new RateLimitError({
message: `Rate limit exceeded. Please try again in ${context.after}` message: `Rate limit exceeded. Please try again in ${Math.ceil(context.ttl / 1000)} seconds`
}); });
}, },
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
@@ -113,3 +113,12 @@ export const requestAccessLimit: RateLimitOptions = {
max: 10, max: 10,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const smtpRateLimit = ({
keyGenerator = (req) => req.realIp
}: Pick<RateLimitOptions, "keyGenerator"> = {}): RateLimitOptions => ({
timeWindow: 20 * 1000,
hook: "preValidation",
max: 1,
keyGenerator
});

View File

@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit } from "@app/server/config/rateLimiter"; import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -11,7 +11,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/signup", url: "/signup",
config: { config: {
rateLimit: inviteUserRateLimit rateLimit: smtpRateLimit()
}, },
method: "POST", method: "POST",
schema: { schema: {
@@ -81,7 +81,9 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/signup-resend", url: "/signup-resend",
config: { config: {
rateLimit: inviteUserRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { membershipId: string }).membershipId
})
}, },
method: "POST", method: "POST",
schema: { schema: {

View File

@@ -2,9 +2,9 @@ import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas"; import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { SanitizedProjectSchema } from "../sanitizedSchemas"; import { SanitizedProjectSchema } from "../sanitizedSchemas";
@@ -47,7 +47,9 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/projects/:projectId/grant-admin-access", url: "/projects/:projectId/grant-admin-access",
config: { config: {
rateLimit: writeLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
})
}, },
schema: { schema: {
params: z.object({ params: z.object({

View File

@@ -2,10 +2,10 @@ import { z } from "zod";
import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas"; import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => { export const registerPasswordRouter = async (server: FastifyZodProvider) => {
@@ -80,7 +80,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/email/password-reset", url: "/email/password-reset",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email: string }).email
})
}, },
schema: { schema: {
body: z.object({ body: z.object({
@@ -224,7 +226,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/email/password-setup", url: "/email/password-setup",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
})
}, },
schema: { schema: {
response: { response: {
@@ -233,6 +237,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
await server.services.password.sendPasswordSetupEmail(req.permission); await server.services.password.sendPasswordSetupEmail(req.permission);
@@ -267,6 +272,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => { handler: async (req, res) => {
await server.services.password.setupPassword(req.body, req.permission); await server.services.password.setupPassword(req.body, req.permission);

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys"; import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@@ -12,7 +12,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/me/emails/code", url: "/me/emails/code",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username: string }).username
})
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { UsersSchema } from "@app/db/schemas"; import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors"; import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -13,7 +13,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
url: "/email/signup", url: "/email/signup",
method: "POST", method: "POST",
config: { config: {
rateLimit: authRateLimit rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email: string }).email
})
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@@ -78,11 +78,14 @@ export default function CodeInputStep({
const resendVerificationEmail = async () => { const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true); setIsResendingVerificationEmail(true);
setIsLoading(true); setIsLoading(true);
await mutateAsync({ email }); try {
setTimeout(() => { await mutateAsync({ email });
setIsLoading(false); } finally {
setIsResendingVerificationEmail(false); setTimeout(() => {
}, 2000); setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 1000);
}
}; };
return ( return (

View File

@@ -22,8 +22,12 @@ export const VerifyEmailPage = () => {
*/ */
const sendVerificationEmail = async () => { const sendVerificationEmail = async () => {
if (email) { if (email) {
await mutateAsync({ email }); try {
setStep(2); await mutateAsync({ email });
setStep(2);
} catch {
setLoading(false);
}
} }
}; };

View File

@@ -159,6 +159,7 @@ export const AddOrgMemberModal = ({
text: "Failed to invite user to org", text: "Failed to invite user to org",
type: "error" type: "error"
}); });
return;
} }
if (serverDetails?.emailConfigured) { if (serverDetails?.emailConfigured) {

View File

@@ -163,6 +163,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
text: "Failed to add user to project", text: "Failed to add user to project",
type: "error" type: "error"
}); });
return;
} }
handlePopUpToggle("addMember", false); handlePopUpToggle("addMember", false);
reset(); reset();