# Vercel Blob handleUpload: never trust the pathname from the client

> With client uploads the browser names the pathname and the token is bound to it. Check it in onBeforeGenerateToken, or any signed-in user can write anywhere in your store.

The standard client-upload example authenticates the user in
`onBeforeGenerateToken` and returns a token. It never looks at `pathname`.

That is a hole. The browser chose that pathname.

## What goes wrong

```ts
onBeforeGenerateToken: async (pathname) => {
  const user = await requireUser(); // authenticated, so...
  return { allowedContentTypes: ["image/png"] };
},
```

A signed-in attacker calls `upload("victim-id/avatar.png", evil, ...)`. They are
authenticated, so they get a token for `victim-id/avatar.png`.

- With `allowOverwrite: true`, they replace the victim's file.
- With the default `false`, they can still squat any pathname before the
  victim does, or fill someone else's folder.
- If your code later trusts "files under `<userId>/`" as that user's files,
  they have planted a file in someone else's account.

## Why you cannot just rewrite it

`handleUpload()` signs the token for the pathname the client sent. Your
callback can accept or refuse it. It cannot change it. A "sanitised" pathname
you compute and return does not change which pathname the token allows.

## The fix: the server mints, then verifies

1. **Reserve.** The browser posts `{ filename, contentType, size }`. The server
   authenticates and builds the key from the session:
   `` `${user.id}/${folder}/${crypto.randomUUID()}-${safeName}` ``.
2. **Upload.** The browser calls `upload(key, file, ...)` with that key.
3. **Verify in `onBeforeGenerateToken`.** Refuse unless the pathname:
   - starts with the caller's own id, compared as a full segment
     (`key.split("/")[0] === userId`, never `startsWith`, or `alice-2` passes
     for `alice`);
   - has the exact shape the server mints (folder, uuid, safe name);
   - ends in the extension for the content type the token will allow.

```ts
onBeforeGenerateToken: async (pathname, clientPayload) => {
  const user = await requireUser();
  const { contentType, size } = JSON.parse(clientPayload ?? "{}");
  assertAllowed(contentType, size);
  assertOwnedKey(pathname, user.id, contentType);
  return {
    allowedContentTypes: [contentType],
    maximumSizeInBytes: size,
    allowOverwrite: false,
    addRandomSuffix: false,
    validUntil: Date.now() + 15 * 60 * 1000,
    tokenPayload: JSON.stringify({ userId: user.id }),
  };
},
```

## Treat clientPayload as hostile too

`clientPayload` is whatever the browser sent. Validate it like a request body.
The safe move is to use it only to *narrow* the token: one content type, the
claimed size. If the browser lied, the store rejects the upload.

`tokenPayload` is different: your server wrote it and it is signed into the
token. That is what `onUploadCompleted` should trust for the owner id.

## Also check on the way out

Any route that reads or deletes a key from a request needs the same owner
check. Authentication proves who is asking. It does not prove the file is
theirs.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
