fix(security): file storage authorization hardening
Three separate issues in the file storage layer:
1. IDOR via client-controlled object key in applicant.saveFileMetadata
and file.replaceFile. Both procedures accepted `bucket` and `objectKey`
from the client and stored them on a new ProjectFile row attached to
the caller's own project. Because file.getDownloadUrl authorizes via
`findFirst({ bucket, objectKey })` -> projectId, an attacker could
bind another team's storage object to their own project row and then
download the foreign object through the legitimate authorization
path. Now both procedures require `bucket === BUCKET_NAME` and the
`objectKey` to start with the project's sanitized title prefix
(matches the prefix that generateObjectKey produces server-side).
New helper `objectKeyBelongsToProject` exported from src/lib/minio.ts;
`sanitizePath` is now exported as well so the helper can reuse it.
2. Missing per-round scope on file.getBulkDownloadUrls. The single-file
getDownloadUrl restricts a juror to files in rounds with sortOrder
<= their assigned round, but the bulk variant only checked that an
Assignment row existed for the project. A juror assigned only to
EVALUATION could pull URLs for LIVE_FINAL/DELIBERATION confidential
files via this endpoint. Now applies the same per-round filter when
the caller's access to the project is jury-only (mentors / team
members / award jurors retain unrestricted access, matching
getDownloadUrl semantics).
3. Same omission on the standalone /api/files/bulk-download REST route.
Same fix applied there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,7 @@ export async function deleteObject(
|
||||
* Sanitize a name for use as a MinIO path segment.
|
||||
* Removes special characters, replaces spaces with underscores, limits length.
|
||||
*/
|
||||
function sanitizePath(name: string): string {
|
||||
export function sanitizePath(name: string): string {
|
||||
return (
|
||||
name
|
||||
.trim()
|
||||
@@ -164,3 +164,16 @@ export function generateMentorObjectKey(
|
||||
return generateObjectKey(projectTitle, fileName, 'mentorship')
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a client-supplied object key actually belongs to a project's
|
||||
* sanitized prefix. Used to prevent cross-tenant binding where an attacker
|
||||
* passes another team's `bucket+objectKey` into a metadata-save procedure.
|
||||
*/
|
||||
export function objectKeyBelongsToProject(
|
||||
objectKey: string,
|
||||
projectTitle: string,
|
||||
): boolean {
|
||||
const sanitized = sanitizePath(projectTitle)
|
||||
return objectKey.startsWith(`${sanitized}/`)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user