Three defense-in-depth gaps from GHSA-3wgj-c2hg-vm6q that the v0.9.5
serving-side fix didn't address:
1. _process_picture_url (utils/oauth.py): MIME was inferred from the
URL extension via mimetypes.guess_type, the upstream Content-Type
was discarded, and there was no allowlist. An SVG picture URL
produced data:image/svg+xml;base64,... in the user's
profile_image_url. Switch to the upstream Content-Type and gate it
against PROFILE_IMAGE_ALLOWED_MIME_TYPES (the same env var the
serving endpoint already uses); fall back to /user.png if the MIME
isn't in the allowlist. Also pass allow_redirects=AIOHTTP_CLIENT_
ALLOW_REDIRECTS on the aiohttp session.get — the existing
validate_url() only checks the initial URL, redirects to internal
targets would otherwise still be followed (same class as the
rh5x-h6pp-cjj6 cluster, sixth call site).
2. update_user_profile_image_url_by_id (models/users.py): SQLAlchemy
write path bypassed the Pydantic form validators, so anything
stored via OAuth or any other non-form caller landed in the column
unchallenged. Run validate_profile_image_url at the storage layer
before the assignment.
3. insert_new_user (models/users.py): same gap on the new-user path
used by Auths.insert_new_auth (OAuth signup, LDAP). Same
storage-layer call to validate_profile_image_url, falling back to
/user.png if the supplied value doesn't pass.
The serving-endpoint allowlist landed in v0.9.5 already broke the
exploit chain matte1782 demonstrated (browser never receives
Content-Type: image/svg+xml), but bad data was still being written
to the DB and the upstream MIME was never trusted. These three
fixes harden the ingestion + storage layers so future serving paths
or DB readers don't have to assume the column is clean.
Reported by matte1782 in GHSA-3wgj-c2hg-vm6q.
Co-authored-by: matte1782 <matte1782@users.noreply.github.com>
* refac: group members table db migration
* refac: group members backend
* refac: group members frontend
* refac: group members frontend integration
* refac: styling