Skip to content

ChatGPT 5.4: Restrict /api/upload/image to approved buckets and validated image uploads #55

@teetangh

Description

@teetangh

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

  1. Replace the free-form bucket input with a server-side enum/allowlist for explicitly supported upload targets.
  2. Enforce allowed MIME types and, if practical, size limits before generating the signed URL.
  3. Align storage paths with the intended domain model (for example profile-images/avatars/{userId}/...) instead of /$bucket/$userId/....
  4. Consider separate endpoints for distinct upload domains if more than one bucket is actually needed.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions