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.
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:
Cause
presign()comparesoptions.pathnameagainst the pathname it decodes outof the delegation token, and the decoder prefers
atob:atobreturns one character per byte, so UTF-8 comes back as mojibake. Node18 and later define
atobglobally, so the correctBufferbranch neverruns on the server, which is where
handleUploadPresignedcalls this.The store is fine. POST a non-ASCII pathname to
/signed-tokenand thedelegation token comes back holding it exactly.
Suggested fix
Decode as UTF-8 in both branches, by preferring
Bufferwhere it exists orby running the
atobresult throughTextDecoder:Affects 2.6.1 through 2.8.0, the current latest;
base64UrlDecodeToStringisunchanged across them. The older
handleUploadclient token flow is fine,since it never decodes a delegation token.