fix(security): close GDPR-export/health-card token guessing and video IDOR
- GDPR export token (admin.exportUserData) and health-card token (health.getOrCreateCard) now get an explicit crypto-random token instead of falling back to Prisma's cuid() default, which is timestamp/counter-derived and not meant for unguessable secrets. - Add IP-based rate limiting to both public token endpoints (/api/gdpr-export/[token], /health-card/[token]) to close the brute-force window. - videos.getByPostId now applies the same hiddenAt/shadowban visibility filter posts.getById already uses, closing an IDOR that let any authenticated user read Mux data for hidden/shadowbanned video posts. Found via a security-reviewer pass over the tRPC/API layer.
This commit is contained in:
1 parent
4414ef12ed
commit
ddf3c2c4e6
5 files changed
+58
-5
No files matched your search
@@ -1156,6 +1156,7 @@ export const adminRouter = router({
|
||||
const exportToken = await ctx.prisma.dataExportToken.create({
|
||||
data: {
|
||||
ownerId: input.ownerId,
|
||||
token: randomUUID().replace(/-/g, ""),
|
||||
storageKey: key,
|
||||
expiresAt: new Date(Date.now() + DATA_EXPORT_TOKEN_TTL_MS),
|
||||
},
|
||||
|
||||
@@ -197,7 +197,7 @@ export const healthRouter = router({
|
||||
const card = await ctx.prisma.healthCard.upsert({
|
||||
where: { petId: input.petId },
|
||||
update: { expiresAt },
|
||||
create: { petId: input.petId, expiresAt },
|
||||
create: { petId: input.petId, expiresAt, token: crypto.randomUUID().replace(/-/g, "") },
|
||||
});
|
||||
return { token: card.token, expiresAt: card.expiresAt };
|
||||
}),
|
||||
|
||||
@@ -80,8 +80,14 @@ export const videosRouter = router({
|
||||
getByPostId: protectedProcedure
|
||||
.input(z.object({ postId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const videoPost = await ctx.prisma.videoPost.findUnique({
|
||||
where: { postId: input.postId },
|
||||
const videoPost = await ctx.prisma.videoPost.findFirst({
|
||||
where: {
|
||||
postId: input.postId,
|
||||
post: {
|
||||
hiddenAt: null,
|
||||
pet: { OR: [{ ownerId: ctx.userId }, { owner: { shadowban: null } }] },
|
||||
},
|
||||
},
|
||||
include: { post: { select: { petId: true, createdAt: true } } },
|
||||
});
|
||||
if (!videoPost) throw new TRPCError({ code: "NOT_FOUND" });
|
||||
|
||||
Reference in new issue
Block a user