Summary
user_avatars and channel_avatars are public buckets that allow image/svg+xml. Any signed-in user can upload an SVG with a script into their own folder and share its public URL. Storage serves SVG inline, so the script runs on the Supabase project origin. docs.plus itself is not affected, because it draws avatars only as <img> or a CSS background. The impact is that anyone can host a phishing page under the project URL.
- Severity: Low
- Area: Supabase Storage buckets, Settings avatar upload
- Source: security review of 2026-10-06, finding L11
Production check (2026-10-06, read-only)
- Both buckets are
public = true and list image/svg+xml in allowed_mime_types.
- The number of SVG objects already stored was not checked.
Where
- Bucket rows:
packages/supabase/scripts/12-buckets.sql:26-31 (user_avatars) and :72-77 (channel_avatars). The header comments at :25 and :71 list SVG too.
- No migration creates either bucket, so production was set up by hand. A migration must
update the rows, not insert them.
- The client passes SVG straight through:
apps/webapp/src/components/settings/utils/prepareAvatarFile.ts:2 (BUCKET_MIME) and :36-41. Once the bucket refuses SVG, this branch would turn every SVG pick into a Storage error.
- The rule that names the allowlist:
packages/supabase/CLAUDE.md:48 ("jpeg/png/gif/webp/svg only"). It forbids widening the list. Narrowing it is allowed.
- No app code reads or writes
channel_avatars.
Fix plan
- Migration. Add one migration that runs
update storage.buckets set allowed_mime_types = '{"image/jpeg","image/png","image/gif","image/webp"}' where id in ('user_avatars', 'channel_avatars');.
- Script mirror. Remove
image/svg+xml from both insert rows and both header comments in packages/supabase/scripts/12-buckets.sql. Regenerate the seed with bun run --filter @docs.plus/supabase_back seed. Never hand-edit seed.sql. No function signature changes, so types needs no run.
- Client. In
prepareAvatarFile.ts, remove image/svg+xml from BUCKET_MIME and delete the SVG pass-through branch. An SVG then takes the canvas path. Where the browser cannot decode it, the user sees the existing "format is not supported" message.
- Docs. Change
packages/supabase/CLAUDE.md:48 to "jpeg/png/gif/webp only".
- Existing objects. In production, list SVG objects read-only:
select bucket_id, name from storage.objects where bucket_id in ('user_avatars', 'channel_avatars') and metadata->>'mimetype' = 'image/svg+xml';
Delete each one through the Storage API (dashboard or a service-role storage.from(bucket).remove([...])). A raw SQL delete is blocked by storage.protect_delete. If a deleted object was a user's avatar.png, also clear that user's avatar_url and avatar_updated_at.
Out of scope
- Pinning upload names to
{uid}/avatar.png. With SVG gone, other names can only hold images, which cannot run script.
- Dropping the unused
channel_avatars bucket. Removing SVG is enough to close this issue.
Acceptance criteria
Verify
- Apply the migration locally:
docker exec -i supabase_db_docsplus_supabase psql -U postgres -d postgres < packages/supabase/migrations/<new file>.sql.
- Check the rows:
select id, allowed_mime_types from storage.buckets where id in ('user_avatars', 'channel_avatars');.
- In the local webapp, upload an SVG and a PNG in Settings → Profile, then remove the avatar.
cd apps/webapp && bun run typecheck.
Related
Generated by Claude Code
Summary
user_avatarsandchannel_avatarsare public buckets that allowimage/svg+xml. Any signed-in user can upload an SVG with a script into their own folder and share its public URL. Storage serves SVG inline, so the script runs on the Supabase project origin. docs.plus itself is not affected, because it draws avatars only as<img>or a CSS background. The impact is that anyone can host a phishing page under the project URL.Production check (2026-10-06, read-only)
public = trueand listimage/svg+xmlinallowed_mime_types.Where
packages/supabase/scripts/12-buckets.sql:26-31(user_avatars) and:72-77(channel_avatars). The header comments at:25and:71list SVG too.updatethe rows, not insert them.apps/webapp/src/components/settings/utils/prepareAvatarFile.ts:2(BUCKET_MIME) and:36-41. Once the bucket refuses SVG, this branch would turn every SVG pick into a Storage error.packages/supabase/CLAUDE.md:48("jpeg/png/gif/webp/svg only"). It forbids widening the list. Narrowing it is allowed.channel_avatars.Fix plan
update storage.buckets set allowed_mime_types = '{"image/jpeg","image/png","image/gif","image/webp"}' where id in ('user_avatars', 'channel_avatars');.image/svg+xmlfrom bothinsertrows and both header comments inpackages/supabase/scripts/12-buckets.sql. Regenerate the seed withbun run --filter @docs.plus/supabase_back seed. Never hand-editseed.sql. No function signature changes, sotypesneeds no run.prepareAvatarFile.ts, removeimage/svg+xmlfromBUCKET_MIMEand delete the SVG pass-through branch. An SVG then takes the canvas path. Where the browser cannot decode it, the user sees the existing "format is not supported" message.packages/supabase/CLAUDE.md:48to "jpeg/png/gif/webp only".select bucket_id, name from storage.objects where bucket_id in ('user_avatars', 'channel_avatars') and metadata->>'mimetype' = 'image/svg+xml';Delete each one through the Storage API (dashboard or a service-role
storage.from(bucket).remove([...])). A raw SQLdeleteis blocked bystorage.protect_delete. If a deleted object was a user'savatar.png, also clear that user'savatar_urlandavatar_updated_at.Out of scope
{uid}/avatar.png. With SVG gone, other names can only hold images, which cannot run script.channel_avatarsbucket. Removing SVG is enough to close this issue.Acceptance criteria
image/svg+xmlto either bucket is refused.Verify
docker exec -i supabase_db_docsplus_supabase psql -U postgres -d postgres < packages/supabase/migrations/<new file>.sql.select id, allowed_mime_types from storage.buckets where id in ('user_avatars', 'channel_avatars');.cd apps/webapp && bun run typecheck.Related
user_avatars)Generated by Claude Code