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:
2026-04-09 21:37:56 +02:00
parent ebeb180f3f
commit 3452464809
2 changed files with 29 additions and 1 deletions
+23 -1
View File
@@ -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() },