fix(security): invalidate sessions on password change and remove hash from permission API responses
- setUserPassword and resetPassword now call activeSession.deleteMany after updating the passwordHash, so any pre-change sessions are immediately revoked (CWE-613 session fixation after credential change) - setUserPermissions and resetUserPermissions now use explicit Prisma select to exclude passwordHash and totpSecret from the returned user object Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc.js";
|
||||
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
||||
import { sendEmail } from "../lib/email.js";
|
||||
import { authRateLimiter } from "../middleware/rate-limit.js";
|
||||
|
||||
const RESET_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
@@ -26,6 +27,14 @@ export const authRouter = createTRPCRouter({
|
||||
requestPasswordReset: publicProcedure
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const rl = await authRateLimiter(input.email);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many password reset attempts. Please wait before trying again.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await ctx.db.user.findUnique({
|
||||
where: { email: input.email },
|
||||
select: { id: true, email: true },
|
||||
@@ -69,6 +78,14 @@ export const authRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const rl = await authRateLimiter(input.token);
|
||||
if (!rl.allowed) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many password reset attempts. Please wait before trying again.",
|
||||
});
|
||||
}
|
||||
|
||||
const record = await ctx.db.passwordResetToken.findUnique({
|
||||
where: { token: input.token },
|
||||
});
|
||||
@@ -86,11 +103,16 @@ export const authRouter = createTRPCRouter({
|
||||
const { hash } = await import("@node-rs/argon2");
|
||||
const passwordHash = await hash(input.password);
|
||||
|
||||
await ctx.db.user.update({
|
||||
const updatedUser = await ctx.db.user.update({
|
||||
where: { email: record.email },
|
||||
data: { passwordHash },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Invalidate all active sessions so any session obtained before the
|
||||
// password reset cannot be reused (CWE-613).
|
||||
await ctx.db.activeSession.deleteMany({ where: { userId: updatedUser.id } });
|
||||
|
||||
await ctx.db.passwordResetToken.update({
|
||||
where: { token: input.token },
|
||||
data: { usedAt: new Date() },
|
||||
|
||||
@@ -171,6 +171,10 @@ export async function setUserPassword(
|
||||
data: { passwordHash },
|
||||
});
|
||||
|
||||
// Invalidate all active sessions so any compromised session cannot be
|
||||
// reused after the password is changed (CWE-613).
|
||||
await ctx.db.activeSession.deleteMany({ where: { userId: input.userId } });
|
||||
|
||||
audit({
|
||||
entityType: "User",
|
||||
entityId: user.id,
|
||||
@@ -381,6 +385,7 @@ export async function setUserPermissions(
|
||||
const user = await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { permissionOverrides: input.overrides ?? Prisma.DbNull },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
});
|
||||
|
||||
audit({
|
||||
@@ -414,6 +419,7 @@ export async function resetUserPermissions(
|
||||
const updated = await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { permissionOverrides: Prisma.DbNull },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
});
|
||||
|
||||
audit({
|
||||
|
||||
Reference in New Issue
Block a user