# File Upload Permissions Scoped to User

- **Pattern:** `ab-002241` (`saas-authorization.resource-auth.file-upload-scoped`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002241
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002241)

## 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.

```typescript
// 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 `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.

  ```typescript
  // 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
- cwe CWE-22
- owasp A01
- nist AC-3

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002241
