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.
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.
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.
ID: saas-authorization.resource-auth.file-upload-scoped
Severity: high
What to look for: Locate file upload handlers — S3 putObject calls, Vercel Blob put(), 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.