fix(storage): percent-encode the object key in every URL, not just purgeCache - #2656
Conversation
…rgeCache
encodeStoragePath exists precisely so that a '#' or '?' in an object key cannot
be read as the start of a fragment or query string, and its own doc comment says
so. It was only wired into purgeCache, while every other method built its URL
straight from _getFinalPath.
getPublicUrl('folder/report#1.png') returned
.../object/public/bucket/folder/report#1.png
so the 'supabase#1.png' was a fragment, the server only ever saw '.../folder/report',
and the caller got a 404 for a file that exists. The same key silently addressed
the wrong object in upload, uploadToSignedUrl, createSignedUploadUrl,
createSignedUrl, the authenticated download path, info and exists.
getPublicUrl also wrapped the whole URL in encodeURI, which escapes a space but
leaves '#' and '?' alone, so it looked encoded while missing the two characters
that actually break routing. With the key encoded per segment that wrapper is
redundant, so this drops it and leaves one mechanism.
Path separators stay literal: encodeStoragePath splits on '/' before encoding,
so folder structure still routes.
5 of the 6 new assertions fail on master.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 SummarySummary by CodeRabbit
Walkthrough
Merge Risk: ⚪ Minimal · up to The change percent-encodes object-key path segments across storage URLs while preserving separators, with passing targeted tests and clean type and formatting checks. No actionable merge-blocking risk remains after normal review. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
One thing I left out of this PR on purpose, since it needs a maintainer call rather than a guess.
`${this.url}/cdn/${encodeStoragePath(id)}${queryString ? `?${queryString}` : ''}`while I did not include it because I cannot verify from here which characters the server accepts in a bucket id, and that is what decides whether this is reachable or merely defensive. What I can say is that it is inconsistent within the same file, and that whoever added the For contrast, Happy to extend this PR to those call sites, or to leave bucket ids alone if the server constrains them enough that encoding would be dead code. Just say which. |
The bug
encodeStoragePathalready exists in this repo, and its doc comment states the reason:It is wired into exactly one call site (
purgeCache). Every other method builds its URL straight from_getFinalPath, which only strips leading slashes:So an object key containing
#or?silently addresses a different object:#1.pngis a fragment. The server only ever sees.../folder/report, and the caller gets a 404 for a file that exists. Keys likereport #1.pdforQ&A?.pngare ordinary user uploads, so this is reachable with no unusual input.The same key breaks the URL in
upload,uploadToSignedUrl,createSignedUploadUrl,createSignedUrl, the authenticated download path,infoandexists— eight call sites in total.getPublicUrladditionally wrapped the whole URL inencodeURI. That escapes a space, so the output looks encoded, while leaving#and?untouched — the two characters that actually break routing.The fix
Apply the existing helper at every call site, exactly as
purgeCachealready does, and drop the now-redundantencodeURIso there is a single mechanism.Path separators keep working:
encodeStoragePathsplits on/before encoding each segment.Verification
New test file
test/object-key-encoding.test.ts, 6 assertions coveringgetPublicUrl(hash, question mark, separators kept literal, self-built query string untouched) plusinfo()andcreateSignedUrl()through an injected fetch.master: 5 of the 6 fail, e.g.Expected .../report%231.png,Received .../report#1.pngFull
storage-jssuite goes from 409 to 415 passing. The 6 failures and 1 snapshot that remain are identical to master on my machine (integration specs that need a local storage server), confirmed withgit stash.tsc --noEmitclean,prettier --checkclean.I could not exercise the integration specs against a real storage server, so the round trip that matters — server decoding each segment back to the original key — rests on the helper's own doc comment and on
purgeCachealready shipping this behaviour. Happy to adjust if any endpoint expects the raw key.