@vercel/blob: presign() decodes the delegation token with atob, so any non-ASCII pathname throws a false scope mismatch
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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