Summary
POST /api/upload/image signs Supabase uploads with the service-role key while accepting an arbitrary client-provided bucket name. That makes the route a generic signed-upload oracle instead of a narrowly scoped image-upload endpoint.
Evidence
- The route trusts
bucket from the request body and defaults to "avatars" if omitted.
backend/routes/api/upload/image.dart:51-53
- It then uses
SUPABASE_SERVICE_ROLE_KEY to request a signed upload URL for /$bucket/$storagePath.
backend/routes/api/upload/image.dart:69-98
- There is no allowlist for buckets, no content-type allowlist, and no server-side size/file-type validation.
backend/routes/api/upload/image.dart:51-145
- The mobile client uses this endpoint as a narrow onboarding image helper and does not provide a bucket override in normal flows.
lib/data/datasources/remote/onboarding_remote_source.dart:149-167
Web parity reference
The web repo mostly avoids this pattern by routing uploads through typed helpers with explicit bucket choices and validation, for example:
- profile images:
/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:1276-1365
- plan images:
/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:803-860
- generic helper exists, but bucket choice is made from server-side call sites, not raw mobile client input:
/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:1414-1458
Why this matters
- Any authenticated caller who knows or guesses a bucket name can ask the backend to sign uploads into that bucket.
- Because the service-role key is used server-side, the route bypasses the normal bucket-scoping discipline expected from the client.
- The route is named and documented as an image uploader, but the backend does not enforce image-only behavior.
Proposed fix
- Replace the free-form
bucket input with a server-side enum/allowlist for explicitly supported upload targets.
- Enforce allowed MIME types and, if practical, size limits before generating the signed URL.
- Align storage paths with the intended domain model (for example
profile-images/avatars/{userId}/...) instead of /$bucket/$userId/....
- Consider separate endpoints for distinct upload domains if more than one bucket is actually needed.
- Add tests proving disallowed buckets and content types are rejected.
Acceptance criteria
- The route cannot be used to mint signed URLs for arbitrary Supabase buckets.
- Upload contracts are explicit, validated, and aligned with real mobile use cases.
Summary
POST /api/upload/imagesigns Supabase uploads with the service-role key while accepting an arbitrary client-provided bucket name. That makes the route a generic signed-upload oracle instead of a narrowly scoped image-upload endpoint.Evidence
bucketfrom the request body and defaults to"avatars"if omitted.backend/routes/api/upload/image.dart:51-53SUPABASE_SERVICE_ROLE_KEYto request a signed upload URL for/$bucket/$storagePath.backend/routes/api/upload/image.dart:69-98backend/routes/api/upload/image.dart:51-145lib/data/datasources/remote/onboarding_remote_source.dart:149-167Web parity reference
The web repo mostly avoids this pattern by routing uploads through typed helpers with explicit bucket choices and validation, for example:
/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:1276-1365/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:803-860/Users/kaustavghosh/Desktop/familiarise_web/lib/supabase.ts:1414-1458Why this matters
Proposed fix
bucketinput with a server-side enum/allowlist for explicitly supported upload targets.profile-images/avatars/{userId}/...) instead of/$bucket/$userId/....Acceptance criteria