Address PR review findings on the original shim:
1. Source-shape gating (avoid shadowing future upstream fixes)
The install now inspects the original implementation source and
only patches when it actually references `self._connection.stop` —
the buggy pattern this shim exists to fix. If a newer SQLAlchemy
ships a working `_terminate_force_close` we leave it alone.
2. Wake the aiosqlite worker, don't just flag it
In aiosqlite ≥0.20 the worker thread blocks on `_tx.get()`. Setting
`_running = False` alone never unblocks it — the queue needs a
sentinel push for the loop to observe the flag. The replacement
now puts `(None, None)` onto `_tx` first and then flips
`_running`, so the worker exits promptly instead of relying on a
subsequent unrelated queue item or GC.
3. Conditional install (no global mutation on non-sqlite deployments)
Move `_install_aiosqlite_compat()` inside the
`if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL` branch in
`internal/db.py`, so PostgreSQL / MySQL / etc. deployments get no
SQLAlchemy monkey-patch at all.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
SQLAlchemy 2.0.36+ added a `terminate_force_close()` path on its
async DB-API adapters, used by the connection pool when a connection
must be invalidated immediately (typically when an in-flight query
was cancelled — request aborted by client, timeout, etc.).
For the aiosqlite dialect, `_terminate_force_close` unconditionally
accesses `self._connection.stop`. That attribute existed on
`aiosqlite.Connection` up to 0.18 (where Connection sub-classed
`threading.Thread`) and was removed in 0.20+ when the worker model
was refactored.
On the version pin Open WebUI ships (sqlalchemy 2.0.48 + aiosqlite
0.21.0) every cancelled aiosqlite call therefore produces a
multi-page
NotImplementedError: terminate_force_close() not implemented by this DBAPI shim
ERROR traceback, drowning real errors in noise even though the
underlying connection is correctly torn down by aiosqlite's own
worker on the next loop tick.
Add `internal/_aiosqlite_compat.install()` which monkey-patches the
shim to understand both APIs:
* aiosqlite ≤ 0.18 — call `Connection.stop()` as before.
* aiosqlite ≥ 0.20 — flip the worker thread's `_running` flag so it
exits on the next iteration (the same end-state the original code
achieved on the old API).
* Anything else — log at debug and fall back to GC.
The patch is idempotent, runs once at db.py import time before the
async engine is created, and is a no-op on installations whose
SQLAlchemy doesn't ship the affected symbol — so it won't break
future upstream rewrites.
https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8
* fix: drop extra='allow' on FolderForm and FolderUpdateForm
These request models were configured to accept arbitrary extra fields,
which were then merged into the folder row via form_data.model_dump().
In insert_new_folder the server-assigned user_id is placed before the
form spread, so a client-supplied user_id in the request body would
override it and the folder would be persisted against another account.
Strictly typed inputs are the correct shape for these endpoints — the
client has no legitimate reason to send fields beyond the declared
ones, and dropping extra='allow' closes the mass-assignment sink at
the validation layer instead of relying on every callsite to merge
fields in the right order.
* fix: reject unknown fields on FolderForm and FolderUpdateForm
Address review feedback: dropping extra='allow' fell back to Pydantic
v2's default extra='ignore', which only silently drops unknown fields
instead of rejecting them. The intent for these request models is a
strict input contract — fail fast when a client sends anything the
server does not expect — so explicitly set extra='forbid'. This also
makes the hardening visible in the form definition rather than implicit
in the default.
These four Redis calls were the only places in the backend still using
bare cache keys. Every other call already namespaces through
REDIS_KEY_PREFIX, which is how multiple Open WebUI instances sharing a
Redis database stay isolated. Two instances with different
TOOL_SERVER_CONNECTIONS or TERMINAL_SERVER_CONNECTIONS would otherwise
clobber each other's cached OpenAPI specs.
* fix: replace brittle profile_image_url allowlist with safe-scheme validation
The previous validation used a hardcoded allowlist of specific static
paths and a single Gravatar prefix. This rejected OWUI's own internal
API paths (e.g. /api/v1/users/{id}/profile/image) and external OAuth
avatar URLs, making it impossible to save user profiles from the admin
panel.
Replace with scheme-based validation that allows relative paths,
HTTP(S) URLs, and data:image URIs while blocking dangerous schemes
like javascript:, file:, and ftp:.
Fixes open-webui#23387
* fix: harden profile image URL validation per review feedback
- Restrict data URIs to safe raster formats (png/jpeg/gif/webp);
SVG is excluded because it can carry embedded scripts.
- Block scheme-relative URLs (//host/path) which browsers resolve
against the current protocol, bypassing the relative-path check.
* fix: use structural validation instead of prefix checks
- Use urlparse for HTTP(S) URLs: gives case-insensitive scheme
matching and rejects bare schemes with no host (e.g. https://).
- Use a compiled regex for data URIs: enforces the ;base64, boundary,
restricts to safe raster formats, and is case-insensitive per spec.
- Removes the startswith-based prefix tuple in favour of proper
URL and data URI parsing.
* fix: validate hostname not netloc, fix misleading comment
- Use parsed.hostname instead of parsed.netloc so URLs like
http://:80/path (non-empty netloc but no actual host) are rejected.
- Update data URI comment to accurately state we validate MIME type
and structure, not base64 payload integrity.
* fix: constrain relative paths to known-safe prefixes
Accepting any relative path starting with / allowed a user to set
their profile_image_url to an arbitrary internal GET endpoint. When
another user (e.g. an admin) views that profile, the browser fires
the GET with the viewer's session cookies — an authenticated GET
trigger surface.
Constrain to known-safe prefixes (/api/v1/users/, /static/) and
exact matches (/user.png, /favicon.png) which are the only relative
paths OWUI itself generates.
* fix: use exact matches and anchored regex, eliminate all prefix wildcarding
Replace all startswith-based path checks with:
- frozenset exact matches for static assets (/user.png, /favicon.png,
/static/favicon.png)
- Anchored regex for the OWUI profile image API route that accepts
only /api/v1/users/{id}/profile/image (no trailing components,
no path traversal across segments)
This eliminates every prefix-based attack surface:
- /api/v1/users/{id}/anything-else is rejected
- /static/../../etc/passwd is rejected
- /api/v1/users/../../admin/config is rejected
- Arbitrary internal GET triggers are no longer possible
* fix: exclude query/fragment delimiters from user-ID regex segment
Change [^/]+ to [^/?#]+ so that inputs like
/api/v1/users/alice?x=1/profile/image are rejected — the browser
would interpret ? as the query string start, making the actual
request target /api/v1/users/alice instead of the intended route.
* Add ownership checks to global task endpoints
- Restrict GET /api/tasks and POST /api/tasks/stop/{task_id} to admin-only
- Add new scoped POST /api/tasks/chat/{chat_id}/stop endpoint with ownership
check so regular users can stop their own chat tasks
- Allow admins to access the scoped chat task endpoints alongside owners
- Update frontend to use the new scoped stop endpoint when a chatId is available
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Handle temporary (local:) chat IDs in scoped task endpoints
Temporary chats use local:<socketId> as chat_id which doesn't exist in
the DB. The scoped endpoints now skip ownership checks for local: IDs
(they aren't enumerable) and use {chat_id:path} to handle the colon in
the URL path.
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Verify session ownership for local: chat IDs and URL-encode chat_id
- For local:<socketId> chat IDs, look up the socket's owner in
SESSION_POOL and verify it matches the requesting user (or admin)
- URL-encode chat_id in frontend fetch calls to handle special
characters (colon in local: IDs) safely
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
---------
Co-authored-by: Claude <noreply@anthropic.com>
The validators.ipv6(ip, private=True) call always returns a falsy ValidationError because validators==0.35.0 does not support the private kwarg for IPv6. This means any hostname resolving to a private IPv6 address (::1, fd00::*, ::ffff:169.254.169.254) bypasses SSRF protection entirely, circumventing the fix for CVE-2025-65958.
Replace both the IPv4 and IPv6 validators-based private checks with Python's stdlib ipaddress module using an allowlist approach (not addr.is_global). This blocks all non-globally-routable addresses — private, loopback, link-local, reserved, multicast, and unspecified — for both IPv4 and IPv6, including IPv4-mapped IPv6 addresses.
Per RFC 4513, a Simple Bind with a non-empty DN but empty password is unauthenticated simple authentication. Many LDAP servers (OpenLDAP default, some AD configs) accept these binds, allowing account takeover without valid credentials.
Rejects empty and whitespace-only passwords before attempting the LDAP bind.
The APIKeyRestrictionMiddleware only inspected the Authorization header for sk- tokens, but get_current_user also reads API keys from cookies and x-api-key headers. This allowed complete bypass of endpoint restrictions by sending the key via an alternate transport.
Moves the restriction check into get_current_user_by_api_key so it runs regardless of how the API key was delivered. Removes the now-redundant middleware.