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>
Both LDAP and OAuth registration checked user count before insert to determine whether to assign admin role. With multiple workers, concurrent first-user registrations could each see zero users and both create admin accounts.
Applies the insert-first-check-after pattern already used by signup_handler: insert with DEFAULT_USER_ROLE, then atomically check get_num_users()==1 and promote only the sole user to admin.
- URL-encodes the OAuth error message when constructing the redirect URL in the OIDC callback handler
- Without encoding, error messages containing spaces, ampersands, or other special characters produce malformed URLs that the frontend cannot parse correctly
- The custom OAuth client callback handler already correctly uses urllib.parse.quote_plus() for the same purpose; this fix brings the OIDC handler in line with that pattern
Co-authored-by: gambletan <tan@gambletan.com>
* fix: replace bare except with except Exception in main.py
* fix: replace bare except with Exception in oauth.py
In Python 3, bare 'except:' is discouraged as it catches all
SystemExit and KeyboardInterrupt exceptions. Changed to 'except Exception:'
to only catch actual exceptions.
* sequential
* zero default
* fix
* fix: preserve absolute paths in sqlite+sqlcipher URLs
Previously, the connection logic incorrectly stripped the leading slash
from `sqlite+sqlcipher` paths, forcibly converting absolute paths
(e.g., `sqlite+sqlcipher:////app/data.db`) into relative paths
(which became `app/data.db`). This caused database initialization failures
when using absolute paths, such as with Docker volume mounts.
This change removes the slash-stripping logic, ensuring that absolute
path conventions (starting with `/`) are respected while maintaining
support for relative paths (which do not start with `/`).
* fix: MCP OAuth 2.1 token exchange and multi-node propagation
Fix two MCP OAuth 2.1 bugs affecting tool server authentication:
1. Token exchange failing with duplicate credentials (#19823)
- Removed explicit client_id/client_secret passing in handle_callback()
- Authlib already has credentials configured during add_client(),
passing them again caused concatenation (e.g., "ID1,ID1") and 401 errors
- Added token validation to detect missing access_token and provide
clear error messages instead of cryptic database constraint errors
2. OAuth clients not propagating across multi-node setups (#19901)
- Updated get_client() and get_client_info() to auto-lazy-load
OAuth clients from the Redis-synced TOOL_SERVER_CONNECTIONS config
- Clients are now instantiated on-demand on any node that needs them
Fixes#19823, #19901
* Update db.py
* Update wrappers.py