File Upload Permissions Scoped to User
Why it matters
File uploads stored in a flat global namespace allow users to access, overwrite, or enumerate other users' files by manipulating the path or key. If the storage path is constructed from user-supplied input rather than the server-side session, path traversal (CWE-22) becomes possible. At minimum, a flat namespace lets user A's upload silently overwrite user B's file if both upload the same filename. OWASP A01 (Broken Access Control) covers this: storage access must be scoped to the authenticated user. NIST 800-53 AC-3 requires access enforcement before any resource operation, including writes to object storage.
Severity rationale
High because flat-namespace uploads expose all users' files to access or overwriting by other users, with severity scaling with the sensitivity of the uploaded content.
Remediation
Construct the storage path server-side from the authenticated session. Never accept a path from user input. Use the user's ID as a namespace prefix so each user's uploads are isolated.
// app/api/upload/route.ts
export async function POST(req: NextRequest) {
const session = await auth();
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const file = await req.blob();
// Path built from session — user cannot influence the namespace
const filename = `users/${session.user.id}/${Date.now()}-${crypto.randomUUID()}`;
const blob = await put(filename, file, { access: 'private' });
return NextResponse.json({ url: blob.url });
}
Also ensure file reads use signed URLs scoped to the requesting user — do not generate public, persistent URLs for private uploads.
Detection
-
ID:
file-upload-scoped -
Severity:
high -
What to look for: Locate file upload handlers — S3
putObjectcalls, Vercel Blobput(), UploadThing handlers, Cloudflare R2 writes, or local filesystem writes. Examine the storage path or key used. Does it include the authenticated user's ID as a namespace prefix? Also check: can users upload to arbitrary paths by supplying a path in the request? -
Pass criteria: All uploaded files are stored under a path or key that includes the authenticated user's ID (e. At least 1 implementation must be verified.g.,
users/${session.user.id}/uploads/${filename}), and the path is constructed server-side from the session, not from user-supplied input. -
Fail criteria: Files are stored in a flat global namespace where different users' files may collide, or the storage path accepts user input that could enable path traversal or overwriting another user's files.
-
Skip (N/A) when: No file upload functionality detected — no S3, Vercel Blob, UploadThing, or filesystem write operations found.
-
Detail on fail:
"File uploads stored without user ID namespace. Users may access, overwrite, or enumerate other users' files."(Note the storage provider and files that implement uploads.) -
Remediation: Always construct the storage path server-side using the authenticated session, never from user-supplied input. Use the user's ID as a namespace prefix.
// app/api/upload/route.ts export async function POST(req: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const file = await req.blob(); const filename = `users/${session.user.id}/${Date.now()}-${crypto.randomUUID()}`; // Path is constructed from session, not user input const blob = await put(filename, file, { access: 'private' }); return NextResponse.json({ url: blob.url }); }Also ensure that read access to files uses signed URLs scoped to the requesting user, not public URLs.
External references
- cwe · CWE-284 — Improper Access Control
- cwe · CWE-22 — Path Traversal
- owasp:2021 · A01
- nist:rev5 · AC-3
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-authorization·automated