Ce mail provient de l'extérieur, restons vigilants

=====================================================================


                            CERT-Renater

                Note d'Information No. 2026/VULN742
_____________________________________________________________________


DATE                : 13/07/2026

HARDWARE PLATFORM(S): /

OPERATING SYSTEM(S): Systems running open-webui (pip) versions prior
                                    to 0.10.0.
 
=====================================================================

https://github.com/open-webui/open-webui/security/advisories/GHSA-j657-m4c4-24jq
https://github.com/open-webui/open-webui/security/advisories/GHSA-frvj-c5qp-xj4w
https://github.com/open-webui/open-webui/security/advisories/GHSA-74h3-cxq7-vc5q
https://github.com/open-webui/open-webui/security/advisories/GHSA-855v-hq7w-jmjw
https://github.com/open-webui/open-webui/security/advisories/GHSA-qg3f-8x3j-ggf2
_____________________________________________________________________




Terminal proxy forwards a spoofable, integrity-unbound user identity
to the upstream (X-User-Id header and ws_terminal session_id query
injection)

High
doge-woof published GHSA-j657-m4c4-24jq 

Package
open-webui (pip)

Affected versions
< 0.10.0

Patched versions
>= 0.10.0


Description

Summary

The terminal proxy in backend/open_webui/routers/terminals.py
forwards the Open WebUI user's identity to the upstream terminal
server / backend coordinator as an authorization claim, with no
cryptographic binding to the session that produced it. The
forwarded identity is attacker-influenceable on both proxy paths:

    HTTP path (proxy_terminal) sets headers['X-User-Id'] = user.id.
Upstreams that trust X-User-Id as identity receive it unsigned,
so an attacker who can reach the upstream by other means (directly,
a compromised peer, SSRF) can spoof it.

    WebSocket path (ws_terminal) is exploitable through Open WebUI
itself, with no "other means" required. It interpolates the path
parameter session_id directly into the upstream URL and then
appends ?user_id=<caller>:

    upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}'
    upstream_url += f'?{urllib.parse.urlencode({"user_id": user.id})}'

session_id is neither validated nor URL-encoded (the HTTP sibling
runs _sanitize_proxy_path; this path runs nothing). An
encoded ?/& smuggled through session_id survives Open WebUI's
single decode and is re-decoded by the upstream, injecting an
attacker-chosen user_id ahead of the appended one. Query parsing
binds the first occurrence, so the backend coordinator resolves
the spoofed user's terminal scope.


Technical Details

The forwarded terminal identity is a bearer-style authorization
claim with no integrity binding, and on the WebSocket path it is
additionally injectable because session_id is concatenated into
the URL without encoding or delimiter validation.


Impact

A normal authenticated user can make the terminal proxy present
another user's identity to the upstream backend coordinator. On
backend coordinator-backed (policy_id) servers that scope terminal
containers by user_id, this reaches another user's terminal scope;
combined with a known active session ID (for example a chat-scoped
session ID surfaced through a shared chat), it allows attaching to
that user's live PTY. The HTTP-path variant additionally allows
identity spoofing at the upstream tier for any deployment whose
upstream trusts X-User-Id.

Appendix: Affected code

    backend/open_webui/routers/terminals.py — proxy_terminal sets headers['X-User-Id'] = user.id with no signature.
    backend/open_webui/routers/terminals.py — ws_terminal builds the upstream URL from an unvalidated, unencoded session_id and appends user_id as a query parameter, allowing query injection.

Appendix: Consolidation

Per the Report Handling policy, this consolidates independent
reports of the same root cause (the forwarded terminal identity
is spoofable / not integrity-bound) into the earliest filing:

    @smoke-wolf (earliest filing) — the X-User-Id HTTP-path
identity is forwarded without integrity binding, spoofable where
the upstream trusts the header.
    @rexpository — the ws_terminal session_id query-injection
vector, proving the forwarded user_id is spoofable through the
Open WebUI proxy itself, with no "reach the upstream by other
means" precondition.


Appendix: Recommended fix

    Validate and URL-encode session_id before building the
upstream URL (urllib.parse.quote(session_id, safe="");
reject ?, #, &, /, %, backslash, control characters). Build
the query string with a URL builder so attacker-controlled
path content cannot precede it.
    Bind the forwarded identity instead of passing a raw
user_id / X-User-Id: emit a short-lived signed claim (for
example HS256 over {uid, iat, aud:server_id} with a key
shared only with the specific upstream) and verify it
upstream.

Severity
High
8.0/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H

CVE ID
CVE-2026-59224

Weaknesses
Weakness CWE-287
Weakness CWE-290

Credits

    @smoke-wolf smoke-wolf Reporter
    @rexpository rexpository Reporter
    @Classic298 Classic298 Remediation developer

_____________________________________________________________________


open-webui terminal proxy path traversal guard bypass via 9x encoded
traversal

High
doge-woof published GHSA-frvj-c5qp-xj4w 

Package
open-webui (pip)

Affected versions
>= 0.9.6, < 0.10.0

Patched versions
>= 0.10.0


Description

AI assistance was used to help inspect the code and prepare this report.
Summary

The fix for GHSA-r2wg-2mcr-66rv is incomplete in v0.9.6 and current main.
backend/open_webui/routers/terminals.py documents _sanitize_proxy_path()
as decoding until stable, but the implementation stops after 8 unquote()
passes. A 9x percent-encoded ../... path parameter remains once-encoded
after the loop, passes the posixpath.normpath() and cleaned.startswith('..')
checks, and is forwarded to the configured terminal server. The upstream
server then receives a decoded traversal path such as /base/../admin/system.


Impact

A user who has access to an admin-configured terminal connection can
bypass the terminal proxy path traversal guard and cause Open WebUI to
forward requests with the configured terminal credentials and X-User-Id
header to paths outside the intended normalized proxy path. For
orchestrator-backed terminal connections the same sanitized path is placed
under /p/{policy_id}/{safe_path}, so the bypass can also target sibling
or parent routes after upstream decoding. This is a bypass of the same
terminal proxy boundary covered by GHSA-r2wg-2mcr-66rv.

This does not require adding a malicious terminal server or convincing an
administrator to weaken settings. The attacker only needs normal access
to an existing configured terminal connection.


Reproduction

The following standalone Python script mirrors the current sanitizer and
uses a local aiohttp server as the terminal-server canary. It shows that
8x encoding is rejected but 9x encoding is accepted and forwarded as a
traversal after the upstream framework decodes the path.

import asyncio, posixpath
from urllib.parse import unquote
from aiohttp import web, ClientSession, ClientTimeout

def sanitize(path):
    decoded = path
    for _ in range(8):
        once = unquote(decoded)
        if once == decoded:
            break
        decoded = once
    cleaned = posixpath.normpath(decoded).lstrip('/')
    if cleaned.startswith('..') or cleaned == '.':
        return None
    return cleaned

def enc(s, rounds):
    out = ''.join(f'%{b:02X}' for b in s.encode())
    for _ in range(rounds - 1):
        out = out.replace('%', '%25')
    return out

async def main():
    async def handler(request):
        return web.json_response({'raw_path': request.raw_path, 'path': request.path})
    app = web.Application()
    app.router.add_route('*', '/{tail:.*}', handler)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 0)
    await site.start()
    port = site._server.sockets[0].getsockname()[1]

    for rounds in (8, 9):
        safe = sanitize(enc('../admin/system', rounds))
        print(rounds, safe)
        if safe:
            url = f'http://127.0.0.1:{port}/base/{safe}'
            async with ClientSession(timeout=ClientTimeout(total=10)) as session:
                async with session.get(url) as response:
                    print(await response.json())
    await runner.cleanup()

asyncio.run(main())

Observed output on current main and v0.9.6 sanitizer:

8 None
9 %2E%2E%2F%61%64%6D%69%6E%2F%73%79%73%74%65%6D
{'raw_path': '/base/..%2Fadmin%2Fsystem', 'path': '/base/../admin/system'}

The 9x encoded path argument is 285 bytes long, so this is not a
megabyte-sized or impractical URL. When sent through the real route,
account for the ASGI server decoding the HTTP path once before filling
the {path:path} parameter: an external request can use one additional
encoding layer so _sanitize_proxy_path() receives the 9x encoded
parameter shown above.


Root Cause / Technical Details

_sanitize_proxy_path() in backend/open_webui/routers/terminals.py
performs this loop:

decoded = path
for _ in range(8):
    once = unquote(decoded)
    if once == decoded:
        break
    decoded = once

The subsequent traversal check is applied only to the value after
those 8 iterations. If the input still contains encoded dot and
slash bytes after the loop, posixpath.normpath() treats them as
ordinary characters rather than path separators. The code then builds
target_url = f'{base_url}/{safe_path}' and sends it with
aiohttp.ClientSession.request(). The upstream server receives and
decodes the forwarded path, turning the accepted %2E%2E%2F... into ../....

The same vulnerable sanitizer is present in v0.9.6, the latest release.
I verified the v0.9.6 backend/open_webui/routers/terminals.py hash
matches current main for this file.


Remediation

Do not rely on a fixed decode-depth cap for a traversal security
boundary. Recommended fixes:

    Decode until stable with a strict input length cap, and reject
if the final value still contains encoded dot, slash, or backslash
separators.
    Reconstruct the allowed relative path from fully decoded
segments: split on path separators, reject empty/current/parent
segments, then join allowed segments with /.
    Add regression tests for at least 9x and 10x encoded ../ payloads,
including a route-level test that accounts for the ASGI server's
initial path decode before the {path:path}
parameter reaches _sanitize_proxy_path().

Severity
High
7.7/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

CVE ID
CVE-2026-59221

Weaknesses
Weakness CWE-22
Weakness CWE-918

Credits

    @DavidCarliez DavidCarliez Reporter
    @Classic298 Classic298 Remediation developer

_____________________________________________________________________


Cross-user code-interpreter and tool execution via unvalidated
Socket.IO event-caller session_id

High
doge-woof published GHSA-74h3-cxq7-vc5q 

Package
open-webui (pip)

Affected versions
< 0.10.0

Patched versions
>= 0.10.0

Description

Summary

An authenticated low-privilege user can execute arbitrary code-interpreter
Python and tools inside another user's authenticated session. The
Socket.IO event-caller (get_event_call) delivers execute:python / execute:tool
events to a client-supplied session_id after only checking that the session
is connected, never that it belongs to the requester. Combined with
ydoc:document:join, which exposes the live socket ids of everyone in a shared
note's collaboration room to any read-access participant, an attacker can
target a victim's session and run attacker-chosen code/tools in the victim's
browser context. When the victim is an administrator, that hijacked context
reaches the admin-only Functions API, whose source is executed server-side,
yielding remote code execution as the server process (root in the default
container).


Affected component

    backend/open_webui/socket/main.py — get_event_call() / __event_caller__
    backend/open_webui/main.py — chat-completion metadata (session_id taken from the request body)

Root cause

The event-caller routes to a caller-controlled session id with no ownership
check:

# backend/open_webui/socket/main.py — get_event_call()
async def __event_caller__(event_data):
    session_id = request_info['session_id']
    if session_id not in SESSION_POOL:                 # only checks the session is connected
        return {'error': 'Client session disconnected.'}
    return await sio.call('events', {...}, to=session_id, ...)   # delivered to that sid

session_id originates from the request body and is never validated
against the authenticated user:

# backend/open_webui/main.py
metadata = {
    'user_id': user.id,                               # server-derived (trustworthy)
    'session_id': form_data.pop('session_id', None),  # client-controlled
    ...
}

SESSION_POOL[session_id] is the user record of whoever owns that
socket. Because the caller checks only membership (in SESSION_POOL),
a request carrying another user's session_id causes
execute:python / execute:tool to be delivered to that other user's
browser.


Reachability

    execute:python / execute:tool are emitted from the code-interpreter
and tool-call paths (utils/middleware.py, tools/builtin.py), all routed
through get_event_call.
    The victim's live session_id is disclosed to any read-access
participant of a shared note via ydoc:document:join.
    POST /api/v1/chat/completions requires only get_verified_user
(the default user role). The attacker uses their own account and a
model / Direct Connection they control to choose the payload.


Impact

    Any victim: arbitrary code-interpreter Python and tool execution
in the victim's authenticated session — the attacker acts with the
victim's identity and origin (full session/account compromise).
    Admin victim: the hijacked admin context reaches
POST /api/v1/functions/create, whose source is exec()'d
server-side → remote code execution as the server process (root in
the default container).

The Functions API is intended administrator code-execution; the
vulnerability here is the cross-user delivery that lets an attacker
drive another user's session — including an admin's — into it. The
primitive is a full session compromise even against non-admin
victims.


Proof of Concept

The reporter's exploit.py reproduced on
ghcr.io/open-webui/open-webui:0.9.6 and a build of the v0.9.6 tag,
confirming blind server-side RCE out-of-band (callback returns
uid=0(root)), using only a low-privilege user account that shared
a note with an admin victim. Preconditions: code interpreter
enabled; attacker shares a note with the victim; victim opens it
while online; admin victim required for server RCE.


Fix

get_event_call must verify the target session belongs to the
requesting user before delivering, not merely that it is connected:

session = SESSION_POOL.get(session_id)
if session is None or session.get('id') != request_info.get('user_id'):
    return {'error': 'Client session disconnected.'}

user_id in the request metadata is server-derived from the
authenticated user, so it is trustworthy. Restricting
ydoc:document:join so it does not disclose other participants'
socket ids is recommended as defence-in-depth.


Affected / Patched

    Affected: < 0.10.0 (last affected release 0.9.6)
    Patched: v0.10.0. get_event_call now verifies the target
session belongs to the requesting user before delivering (session
is None or session.get('id') != request_info.get('user_id')),
using the server-derived user_id from the request metadata. The
recommended ydoc:document:join sid-disclosure restriction is
defence-in-depth and independent of this fix; the ownership check
closes the cross-user delivery regardless of whether the victim's
sid is known.

Severity
High
7.7/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N

CVE ID
CVE-2026-59216

Weaknesses
Weakness CWE-94
Weakness CWE-200
Weakness CWE-639
Weakness CWE-862

Credits

    @waiveyk waiveyk Reporter
    @Classic298 Classic298 Remediation developer


_____________________________________________________________________



Realtime endpoints accept Redis-revoked JWTs after signout/backchannel
logout
High
doge-woof published GHSA-855v-hq7w-jmjw 

Package
open-webui (pip)

Affected versions
>= 0.9.0, < 0.10.0

Patched versions
>= 0.10.0


Description

Summary

With Redis configured, Open WebUI supports JWT revocation:
POST /api/v1/auths/signout (per-token jti) and OIDC back-channel
logout (per-user revoked_at) record revocations in Redis, and
HTTP auth (get_current_user) rejects revoked tokens with 401. The
realtime authentication surfaces do not perform this check:
Socket.IO connect / user-join / join-channels / join-note and the
terminal websocket first-message auth validate tokens with
decode_token() only (signature + expiry). A JWT revoked by sign-out
or back-channel logout therefore continues to authenticate new
realtime connections, even though the same token is rejected on
HTTP.


Affected component

    backend/open_webui/socket/main.py — Socket.IO connect,
user-join, join-channels, join-note
    backend/open_webui/routers/terminals.py — terminal websocket
first-message auth
    backend/open_webui/utils/auth.py — the revocation check was
applied to HTTP only


Root cause

HTTP auth enforces revocation:

# utils/auth.py — get_current_user
if data.get('jti') and not await is_valid_token(request, data):
    raise HTTPException(status_code=401, detail='Invalid token')

Realtime auth calls decode_token() only, which verifies
signature + expiry but never consults the Redis revocation
keys ({prefix}:auth:token:{jti}:revoked, {prefix}:auth:user:{id}:revoked_at):

# socket/main.py — connect / user-join / join-channels / join-note
data = decode_token(auth['token'])
# routers/terminals.py — _resolve_authenticated_connection
data = decode_token(token)


Impact

A JWT revoked by user sign-out or OIDC back-channel logout still
authenticates new realtime connections. A stolen token therefore
retains realtime access after the victim signs out or the IdP
performs back-channel logout — the very remediation for a compromised
token. The token can populate SESSION_POOL as the victim, join their
user/channel/note rooms (receiving realtime channel messages,
collaborative-note updates and presence), drive socket-level
collaboration as the victim, and pass terminal websocket authentication
when terminal servers are configured. HTTP remains correctly
protected (401), so REST data and state-changing REST endpoints
are not reachable with the revoked token.


Proof of Concept

Reporter PoC on a Redis-backed deployment (v0.9.6 and main): after
POST /api/v1/auths/signout, HTTP returns 401 for the token while a
Socket.IO user-join with the same token still authenticates, and the
terminal WS reaches terminal-server lookup rather than rejecting it
as Invalid token.


Fix

Apply the revocation check on the realtime paths. The logic is factored
into is_token_revoked(redis, decoded) (covering per-token jti and
per-user revoked_at); the Socket.IO handlers and the terminal WS reject
tokens that fail it, using the main app Redis where revocations are
stored. HTTP is_valid_token delegates to the same helper, so HTTP
behaviour is unchanged.


Affected / Patched

    Affected: >= 0.9.0, < 0.10.0, and only when Redis is configured (without
Redis, per-token revocation is not supported and sign-out does not
invalidate JWTs by design).
    Patched: v0.10.0. The revocation check (is_valid_token, covering
per-token jti and per-user revoked_at) is applied on Socket.IO
connect / user-join / join-channels / join-note and the terminal websocket
first-message auth, using the main app Redis where revocations are stored.
HTTP is_valid_token delegates to the same logic, so HTTP behaviour is
unchanged.

Severity
High
7.1/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

CVE ID
CVE-2026-59219

Weaknesses
No CWEs

Credits

    @huslayer826 huslayer826 Reporter
    @Classic298 Classic298 Coordinator


_____________________________________________________________________



Realtime endpoints accept Redis-revoked JWTs after signout/backchannel
logout

High
doge-woof published GHSA-855v-hq7w-jmjw

Package
open-webui (pip)

Affected versions
>= 0.9.0, < 0.10.0

Patched versions
>= 0.10.0


Description

Summary

With Redis configured, Open WebUI supports JWT revocation:
POST /api/v1/auths/signout (per-token jti) and OIDC back-channel
logout (per-user revoked_at) record revocations in Redis, and
HTTP auth (get_current_user) rejects revoked tokens with 401. The
realtime authentication surfaces do not perform this check:
Socket.IO connect / user-join / join-channels / join-note and the
terminal websocket first-message auth validate tokens with
decode_token() only (signature + expiry). A JWT revoked by sign-out
or back-channel logout therefore continues to authenticate new
realtime connections, even though the same token is rejected on
HTTP.


Affected component

    backend/open_webui/socket/main.py — Socket.IO connect, user-join,
join-channels, join-note
    backend/open_webui/routers/terminals.py — terminal websocket
first-message auth
    backend/open_webui/utils/auth.py — the revocation check was
applied to HTTP only


Root cause

HTTP auth enforces revocation:

# utils/auth.py — get_current_user
if data.get('jti') and not await is_valid_token(request, data):
    raise HTTPException(status_code=401, detail='Invalid token')

Realtime auth calls decode_token() only, which verifies signature + expiry but never consults the Redis revocation keys ({prefix}:auth:token:{jti}:revoked, {prefix}:auth:user:{id}:revoked_at):

# socket/main.py — connect / user-join / join-channels / join-note
data = decode_token(auth['token'])
# routers/terminals.py — _resolve_authenticated_connection
data = decode_token(token)

Impact

A JWT revoked by user sign-out or OIDC back-channel logout still
authenticates new realtime connections. A stolen token therefore
retains realtime access after the victim signs out or the IdP performs
back-channel logout — the very remediation for a compromised token.
The token can populate SESSION_POOL as the victim, join their
user/channel/note rooms (receiving realtime channel messages,
collaborative-note updates and presence), drive socket-level
collaboration as the victim, and pass terminal websocket authentication
when terminal servers are configured. HTTP remains correctly protected
(401), so REST data and state-changing REST endpoints are not
reachable with the revoked token.


Proof of Concept

Reporter PoC on a Redis-backed deployment (v0.9.6 and main): after
POST /api/v1/auths/signout, HTTP returns 401 for the token while a
Socket.IO user-join with the same token still authenticates, and the
terminal WS reaches terminal-server lookup rather than rejecting it
as Invalid token.


Fix

Apply the revocation check on the realtime paths. The logic is
factored into is_token_revoked(redis, decoded) (covering per-token
jti and per-user revoked_at); the Socket.IO handlers and the
terminal WS reject tokens that fail it, using the main app Redis
where revocations are stored. HTTP is_valid_token delegates to
the same helper, so HTTP behaviour is unchanged.


Affected / Patched

    Affected: >= 0.9.0, < 0.10.0, and only when Redis is configured
(without Redis, per-token revocation is not supported and sign-out
does not invalidate JWTs by design).
    Patched: v0.10.0. The revocation check (is_valid_token, covering
per-token jti and per-user revoked_at) is applied on Socket.IO
connect / user-join / join-channels / join-note and the terminal
websocket first-message auth, using the main app Redis where
revocations are stored. HTTP is_valid_token delegates to the same
logic, so HTTP behaviour is unchanged.

Severity
High
7.1/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

CVE ID
CVE-2026-59219

Weaknesses
No CWEs

Credits

    @huslayer826 huslayer826 Reporter
    @Classic298 Classic298 Coordinator

_____________________________________________________________________


`WEB_FETCH_FILTER_LIST` host allow/block filter bypassable via URL
path and non-label-boundary matching

Moderate
doge-woof published GHSA-qg3f-8x3j-ggf2 

Package
open-webui (pip)

Affected versions
< 0.10.0

Patched versions
>= 0.10.0


Description

Summary

The administrator-configured WEB_FETCH_FILTER_LIST (the allow/block list
applied to server-side web fetches: RAG URL ingestion, URL-to-markdown,
web-search content fetch) matches hostnames incorrectly, so the filter
can be bypassed.


Details

is_string_allowed (backend/open_webui/utils/misc.py) matches with
str.endswith(...), and the primary web-fetch call site
(backend/open_webui/retrieval/web/utils.py) called it with the full
URL string, not the hostname:

    Blocklist bypass via path. A blocklist entry !internal.example.com
only matches a URL that ends with that string. Any URL with a path
(https://internal.example.com/x) ends with /x, so the entry never matches
and the fetch proceeds. The blocklist effectively only stopped path-less
URLs.
    Allowlist false-reject and bypass. An allowlist company.com rejected
the legitimate https://api.company.com/status and admitted
https://attacker.example/path/company.com.
    Non-label-boundary matching at the hostname-shaped call site
(retrieval/web/main.py): endswith('corp.com') also matched evilcorp.com,
and 10.0.0.1 matched 110.0.0.1.


Impact

An authenticated user able to trigger a server-side web fetch can reach
hosts the administrator intended to block with WEB_FETCH_FILTER_LIST.

Open WebUI's primary SSRF protection is a separate, always-on guard that
rejects any URL resolving to a non-global IP (validate_url and the
connection-layer _ssrf_safe_new_conn, active whenever
ENABLE_RAG_LOCAL_WEB_FETCH is off, the default). That guard is unaffected
by this issue and continues to block loopback, RFC1918 and link-local
addresses, including the 169.254.169.254 cloud-metadata endpoint. This
bypass therefore does not grant access to those internal targets. What it
defeats is the administrator's ability to block specific publicly-resolvable
hosts (internal services reachable from the server over a public IP, e.g.
split-horizon DNS or internal PaaS endpoints) and to enforce an allowlist.
Fetched content is returned to the requester, so for hosts reachable from
the server's network position this is a read/content-disclosure SSRF against
the admin-blocked host.


Patch

Matching is now performed on the parsed hostname using DNS label boundaries.
A dedicated is_host_allowed(host, ...) matches an entry only when host and
entry are equal or the entry is a parent domain
(host == entry or host.endswith('.' + entry)), so corp.com matches
api.corp.com but not evilcorp.com, and IP entries match only the identical
address. Both web-fetch call sites pass the parsed hostname rather than the
full URL. The generic is_string_allowed is retained unchanged for unrelated
non-host filters.


Credit

Reported by @addcontent.


Severity
Moderate
4.3/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

CVE ID
CVE-2026-59223

Weaknesses
Weakness CWE-693

Credits

    @addcontent addcontent Reporter
    @Classic298 Classic298 Remediation developer



=========================================================

+ CERT-RENATER        |    tel : 01-53-94-20-44         +
+ 23/25 Rue Daviel    |    fax : 01-53-94-20-41         +
+ 75013 Paris         |   email:cert@support.renater.fr +
=========================================================




