GET /api/v1/channels/{id}/messages/{message_id}/thread authorized only the URL channel, but get_messages_by_parent_id() appended the thread parent (loaded by id) without checking it belonged to that channel, so a caller could read a message from a channel they cannot access by passing its id as the thread root. Require the parent to be in the requested channel before returning it, and reject a posted parent_id/reply_to_id that does not belong to the channel.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace per-message database queries with batch IN-clause queries in
channel message handlers. This eliminates the N+1 query pattern that
caused ~102 queries per channel page load (50 messages × 2 queries each).
Changes:
- Add get_reactions_by_message_ids() to MessageTable: single query
fetches all reactions for multiple messages using IN clause with
User JOIN, returns dict[message_id, list[Reactions]]
- Add get_thread_reply_counts_by_message_ids() to MessageTable: single
GROUP BY aggregate query returns (count, max_created_at) per parent,
replacing full object loads just to call len()
- Refactor get_channel_messages(): 102 → 4 queries per page
- Refactor get_pinned_channel_messages(): 22 → 3 queries per page
- Refactor get_channel_thread_messages(): 53 → 4 queries per page
- Refactor send_notification(): N+1 membership check → batch set lookup
The history diff, delete, and version-restore routes authorize the URL
prompt_id but then act on a caller-supplied history/version id without
checking it belongs to that prompt (IDOR). Filter by prompt_id in
compute_diff and delete_history_entry, and reject a cross-prompt version_id
in update_prompt_version.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SVG-XSS hardening introduced in f5f4b5895 correctly rejects
data:image/svg+xml URIs on new input, but also caused a
pydantic_core.ValidationError when reading pre-existing models from
the database that had SVG data URIs stored as their profile images.
This ValidationError propagated unhandled through _to_model_model and
get_all_models, crashing the entire /api/models endpoint with HTTP 500
and leaving users with no models available in the UI.
Fix:
- Wrap validate_profile_image_url() in a try/except ValueError inside
ModelMeta.check_profile_image_url. Legacy entries are cleared to None
with a warning log instead of raising — the /model/profile/image API
endpoint already falls back to /static/favicon.png when the value is
empty.
- Default ModelMeta.profile_image_url to None instead of hardcoding
/static/favicon.png, since the serving endpoint handles the fallback.
- Add a per-model try/except in ModelsTable.get_all_models so that any
future unexpected validation failure on a single record skips that
model with an error log rather than aborting the entire list.
insert_chat_files() stored any caller-supplied file_id with no ownership
check, so a user could attach another user's file to their own chat and
then read it through the shared-chat access path in has_access_to_file().
Filter file_ids to those the caller owns, is admin for, or can read.
Also repairs an UnboundLocalError introduced in 260ead64d: the existing
duplicate-check referenced `session` before it was assigned (db=session),
so the function threw on every call and no chat_file rows were persisted.
ModelMeta.profile_image_url now runs validate_profile_image_url, rejecting SVG/script data URIs (matching UserUpdateForm and ChannelWebhookForm). The /model/profile/image endpoint enforces the PROFILE_IMAGE_ALLOWED_MIME_TYPES allowlist and sets X-Content-Type-Options: nosniff, so an SVG data URI can no longer be served inline on-origin. Closes the fourth profile-image XSS sink missed by the user and webhook fixes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>