vercel / vercel/storage

@vercel/blob: presign() decodes the delegation token with atob, so any non-ASCII pathname throws a false scope mismatch

Open Beginner friendly
#1,101 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
595
Forks
101
Avg merge
2h 24m
Merged PRs (30d)
1

Description

Uploading a file whose name holds any non-ASCII character fails with a scope
mismatch that isn't real. The path passed in and the path in the token are
the same string. The token is only being read back wrong.

Danish, German, Spanish, French, Greek, Cyrillic, Japanese and emoji names
all hit it, so those users cannot upload at all.

Reproducing

No account, no store, no network. Against 2.8.0 on Node 22:

import { presignUrl } from '@vercel/blob';

const pathname = 'uploads/Skærmbillede.png';
const claims = { pathname, operations: ['put'], validUntil: Date.now() + 600_000 };
const delegationToken =
   Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url') + '.sig';

await presignUrl({ delegationToken, clientSigningToken: 'cst' }, { pathname, operation: 'put' });
uploads/plain-ascii.png       accepted
uploads/Skærmbillede.png      expected `uploads/Skærmbillede.png`, got `uploads/Skærmbillede.png`
uploads/Снимок.png            expected `uploads/Снимок.png`, got `uploads/Снимок.png`
uploads/スクリーンショット.png    expected `uploads/ã¹ã¯ãªã¼ã³ã·ã§ãã.png`, got `uploads/スクリーンショット.png`

Cause

presign() compares options.pathname against the pathname it decodes out
of the delegation token, and the decoder prefers atob:

// dist/chunk-YYMLUMXS.js, base64UrlDecodeToString
if (typeof atob === "function") {
   return atob(base64);
}
if (typeof Buffer !== "undefined") {
   return Buffer.from(base64, "base64").toString("utf8");
}

atob returns one character per byte, so UTF-8 comes back as mojibake. Node
18 and later define atob globally, so the correct Buffer branch never
runs on the server, which is where handleUploadPresigned calls this.

The store is fine. POST a non-ASCII pathname to /signed-token and the
delegation token comes back holding it exactly.

Suggested fix

Decode as UTF-8 in both branches, by preferring Buffer where it exists or
by running the atob result through TextDecoder:

new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)))

Affects 2.6.1 through 2.8.0, the current latest; base64UrlDecodeToString is
unchanged across them. The older handleUpload client token flow is fine,
since it never decodes a delegation token.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with dist/chunk-YYMLUMXS.js and the base64UrlDecodeToString entry point, then run the supplied Node 22 reproduction. Update the delegation-token decoding so UTF-8 pathnames remain unchanged in the atob and Buffer environments, and verify ASCII and non-ASCII pathnames no longer produce a false scope mismatch.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, typescript
Domain
api, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.