security: rate-limit IP-keyed, fail-closed on empty key (#37)
Rate-limiter now accepts string | string[] so callers can key on multiple buckets simultaneously. If any bucket is exhausted the request is denied, which lets login/TOTP/reset-password throttle on BOTH user identifier and source IP without either becoming a bypass. Fail-closed: empty/whitespace-only keys now deny by default instead of silently allowing unbounded attempts (was CWE-307 gap). Degraded-fallback divisor reduced from /10 to /2 — the old aggressive clamp forced-logged-out legitimate users during brief Redis outages; /2 still meaningfully slows distributed brute-force. Callers updated: - auth.ts (login): both email: and ip: buckets - auth router requestPasswordReset: email + IP - auth router resetPassword: IP before lookup, email-reset after - invite router getInvite/acceptInvite: IP - user-self-service verifyTotp: userId + IP TRPCContext now carries clientIp; web tRPC route extracts it from X-Forwarded-For / X-Real-IP. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -86,12 +86,15 @@ export const inviteRouter = createTRPCRouter({
|
||||
getInvite: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const rl = await authRateLimiter(input.token);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many attempts. Please wait before trying again.",
|
||||
});
|
||||
const ipKey = ctx.clientIp ? `ip:${ctx.clientIp}` : "";
|
||||
if (ipKey) {
|
||||
const rl = await authRateLimiter(ipKey);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many attempts. Please wait before trying again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await ctx.db.inviteToken.findUnique({
|
||||
@@ -115,12 +118,15 @@ export const inviteRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const rl = await authRateLimiter(input.token);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many attempts. Please wait before trying again.",
|
||||
});
|
||||
const ipKey = ctx.clientIp ? `ip:${ctx.clientIp}` : "";
|
||||
if (ipKey) {
|
||||
const rl = await authRateLimiter(ipKey);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many attempts. Please wait before trying again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await ctx.db.inviteToken.findUnique({
|
||||
|
||||
Reference in New Issue
Block a user