fix(security): restrict file.replaceFile to admins + team members only

Replace was previously accepted from anyone with a relationship to the
project: jury (assignment), mentor (mentorAssignment), or team member.
That allowed jurors and mentors to swap a team's submission, with the
attacker-supplied bucket+objectKey pointing at any object they had
uploaded elsewhere.

Now only admins and the team itself (submitter or TeamMember) can
replace files. Jurors and mentors remain read-only on submissions.
The legitimate UI flow (team-lead replacing files from the applicant
dashboard) is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-29 03:13:11 +02:00
parent 48d29d4a6b
commit 765bdf9f9e

View File

@@ -517,20 +517,14 @@ export const fileRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER', 'AWARD_MASTER'].includes(ctx.user.role)
// Replace is a write operation on the team's submission. Only admins
// and the team itself (submitter or team member) may replace files —
// jurors and mentors are read-only on project files even though they
// can see them. Observers/Award masters are also read-only.
const isAdmin = ctx.user.role === 'SUPER_ADMIN' || ctx.user.role === 'PROGRAM_ADMIN'
if (!isAdminOrObserver) {
// Check user has access to the project (assigned or team member)
const [assignment, mentorAssignment, teamMembership] = await Promise.all([
ctx.prisma.assignment.findFirst({
where: { userId: ctx.user.id, projectId: input.projectId },
select: { id: true },
}),
ctx.prisma.mentorAssignment.findFirst({
where: { mentorId: ctx.user.id, projectId: input.projectId },
select: { id: true },
}),
ctx.prisma.project.findFirst({
if (!isAdmin) {
const teamMembership = await ctx.prisma.project.findFirst({
where: {
id: input.projectId,
OR: [
@@ -539,10 +533,9 @@ export const fileRouter = router({
],
},
select: { id: true },
}),
])
})
if (!assignment && !mentorAssignment && !teamMembership) {
if (!teamMembership) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not have access to replace files for this project',