fix(storage): url-encode object keys in file API request paths - #2576
fix(storage): url-encode object keys in file API request paths#2576Sy-D wants to merge 1 commit into
Conversation
📝 WalkthroughSummary by CodeRabbit
Walkthrough
Sequence Diagram(s)sequenceDiagram
participant StorageClient
participant StorageFileApi
participant StorageEndpoint
StorageClient->>StorageFileApi: request URL for object key
StorageFileApi->>StorageFileApi: encode bucket and object path
StorageFileApi->>StorageEndpoint: send URL with encoded pathname
StorageEndpoint-->>StorageClient: return response or generated URL
Possibly related PRs
Suggested labels: 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 |
Object keys may contain characters the Storage server accepts but that change the meaning of a URL. Verified against a local Storage server, `?` ` ` `&` `=` `+` `,` `;` `@` `:` `$` `(` `)` `'` `*` `!` are all valid in a key. `_getFinalPath` interpolated them raw, so `folder/a?b.txt` went out as `/object/bucket/folder/a?b.txt` and the server saw the key `folder/a` with a querystring — a different object, with no error. This is the same failure supabase#2545 fixed for the CDN purge methods; the helper added there was never applied to the rest of the file API. Encode in `_getFinalPath` so every call site is covered. This drops the now-redundant `encodeStoragePath` in `purgeCache` and the `encodeURI` in `getPublicUrl`, which would otherwise double-encode. The `encodeURI` in `createSignedUrl` stays: the server returns that path with `%20` already decoded to a literal space, so it is still required there.
1709090 to
2132350
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/storage-js/test/encode-object-key.test.ts`:
- Around line 58-62: Remove the hardcoded SERVICE_KEY service-role JWT from the
test and load the test-only credential from the established environment or CI
secret mechanism before constructing StorageClient. Preserve the Authorization
bearer format, fail clearly when the required secret is absent, and rotate the
exposed token if it was used outside an isolated local fixture.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 95773fc0-8154-4a7e-aa28-84e52c7b4b3c
📒 Files selected for processing (2)
packages/core/storage-js/src/packages/StorageFileApi.tspackages/core/storage-js/test/encode-object-key.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/storage-js/src/packages/StorageFileApi.ts
| // secret key - bypasses RLS for testing | ||
| const SERVICE_KEY = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' | ||
|
|
||
| const storage = new StorageClient(SERVER_URL, { Authorization: `Bearer ${SERVICE_KEY}` }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove the committed service-role JWT.
This hardcoded bearer credential grants service_role access and is sent to the Storage server. Inject a test-only key through environment/CI secrets instead, and rotate this token if it has been used outside an isolated local fixture.
🧰 Tools
🪛 Betterleaks (1.7.0)
[high] 60-60: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/storage-js/test/encode-object-key.test.ts` around lines 58 -
62, Remove the hardcoded SERVICE_KEY service-role JWT from the test and load the
test-only credential from the established environment or CI secret mechanism
before constructing StorageClient. Preserve the Authorization bearer format,
fail clearly when the required secret is absent, and rotate the exposed token if
it was used outside an isolated local fixture.
Sources: Path instructions, Linters/SAST tools
|
Thanks — I looked at this one carefully, but I do not think it applies here. The token is not a credential. Its payload is: { "iss": "supabase-demo", "role": "service_role", "exp": 1983812996 }That is the public demo key the Supabase CLI ships for local development, not a secret belonging to any project. The exact same string is already hardcoded in three other test files in this package:
each with the same Loading it from an env var or CI secret instead would make this one file behave differently from the other three, and add a failure mode (missing secret) for a value that is a public constant. There is also nothing to rotate. Happy to switch if maintainers would rather move all four files to a shared constant or an env lookup — but that seems like a separate change, not something this PR should do on its own. |
Description
Object keys may contain characters the Storage server accepts but that change the meaning of a URL.
_getFinalPathinterpolated them into the request path raw, so a valid key was sent as key-plus-querystring and the server resolved a different object, with no error:This is the same failure #2545 fixed for the CDN purge methods — the helper and rationale added there (
encodeStoragePath) were never applied to the rest of the file API.Affected:
upload/update,createSignedUploadUrl,createSignedUrl,createSignedUrls,info,exists,getPublicUrl.Which characters this is about
I probed a local Storage server rather than guessing. It accepts these in an object key and stores them verbatim:
?space&=+,()@:;$*!and the apostropheIt rejects
#,%,[,],~, backtick,^,|and all non-ASCII with400 InvalidKey. So#was never the interesting case —?, a space,&,=and+are.What changed
Encoding moved into
_getFinalPath, so every call site is covered and a new one cannot silently reintroduce the bug. All 8 uses of_getFinalPathfeed a URL; none goes into a request body.Two follow-on removals, both required to avoid double-encoding:
purgeCacheno longer wraps withencodeStoragePath(now redundant).getPublicUrlno longer wraps withencodeURI. That wrapper was insufficient anyway —encodeURIleaves?,&,=and+untouched — and it would double-encode the new output.The
encodeURIincreateSignedUrlstays. It wrapsdata.signedURLfrom the server response, and the server returns that path with%20already decoded to a literal space, so it is still required there.Testing
packages/core/storage-js/test/encode-object-key.test.ts— 4 unit tests (capturedfetch, URL shape) plus 3 integration tests against the Docker-backed Storage server that uploadfolder/a?b&c=d e+f.txtand read the same object back.On
master, 6 of the 7 fail. The integration failure is the telling one: the upload lands as an object literally nameda, andlist("folder")returns["a"].Full
nx test:storage storage-js:masterBoth fully green; the delta is exactly this suite and its 7 tests.
nx lint storage-jsreports 15 errors both here and onmaster— all pre-existing.nx format:checkandnx build storage-jspass.A server-side bug this does not fix
With this patch,
upload,exists,info,downloadandgetPublicUrlwork for every accepted character.createSignedUrldoes not: it returns 400 for?&=+,;@:$, and works only for keys whose sole escape is%20— plus the characters that need no escaping at all.The cause is server-side: the token is signed over the percent-encoded key, but the request is validated against the decoded path, so the signature never matches. The URL the server itself returns fails identically, so no client-side encoding can work around it. Two tests pin this behaviour explicitly, including the 400, so the limitation is documented rather than silently shipped. Happy to open a separate
storage-apiissue if that would help.Also worth knowing
Since
%is an invalid key character, callers who percent-encode their keys before passing them in were already broken and remain so — they now fail at the client-encoding step instead of reaching the server. I do not believe that is a regression, but it is a behaviour change worth a maintainer eye.Type of Change
Checklist
nx format)nx test:storage storage-js, fully green)nx build storage-js)