Your Elixir app is shouting its OpenAI API key across a crowded café because someone forgot to check the barista's ID badge
CVE-2026-107685 is an improper certificate validation flaw in the WebSocket streaming client of agentjido req_llm, the dominant Elixir library for LLM interactions (766K+ hex.pm downloads, 54 dependents). Versions 1.9.0 through 1.28.0 invoke WebSockex.start/4 without TLS options, and WebSockex 0.5.x defaults to insecure: true — meaning verify: :verify_none. Worse, the req_llm option allowlist *actively strips* any caller-supplied cacerts or ssl_options, so even a security-conscious developer cannot turn verification on. The OpenAI Responses, Realtime, and Codex WebSocket paths all flow through this client and transmit the API key or OAuth bearer token in the authorization header. HTTP/SSE streaming via Finch and the HTTP/2 duplex session via Mint are not affected.
The GHSA rates this 7.4 HIGH (CVSS:3.1, AC:H), and some CVSS 4.0 aggregators cite 9.1 CRITICAL (AT:P). Both acknowledge the on-path attacker requirement, but the 9.1 overstates real-world risk. The default openai_stream_transport is :sse — safe — so most req_llm users are not exposed unless they explicitly opted into :websocket or use the Realtime API (which has no SSE alternative). The on-path requirement is genuine friction in modern cloud deployments. We assess this as HIGH at 6.5: the impact on affected deployments is severe (full API-key theft, prompt/response interception, response tampering), but the reachable population is narrowed by the default-safe transport and the MitM prerequisite.
4 steps from start to impact.
Victim app opts into WebSocket transport
provider_options: [openai_stream_transport: :websocket] for the Responses/Codex APIs, or calls ReqLLM.Providers.OpenAI.Realtime.connect/2 (which is WebSocket-only). The application's mix.lock pins a vulnerable req_llm version. No user action triggers this — the transport choice is made at deploy time or in application code.- Application uses req_llm ≥1.9.0 <1.29.0
- WebSocket transport is selected (explicit opt-in or Realtime API)
- Default transport is
:ssevia Finch, which verifies TLS — most deployments never flip this switch - Realtime API adoption in Elixir is still early; the feature landed in req_llm 1.9.0
mix audit, Dependabot, Snyk) flag the GHSA advisory. Static analysis of mix.lock for version range match.Attacker achieves on-path position
api.openai.com (or the configured OpenAI base URL). In cloud environments this requires DNS poisoning, BGP hijack, a compromised upstream router, or ARP spoofing on the same L2 segment. In less-hardened environments (coffee-shop Wi-Fi, shared hosting, poorly segmented corporate LANs), this is easier.- Network-level access between the app server and OpenAI endpoints
- Ability to intercept and proxy TCP connections on port 443
- Production Elixir apps typically run in cloud VPCs (AWS, GCP, Fly.io) with hardened DNS and no shared L2 with untrusted hosts
- BGP hijacking and DNS poisoning are high-skill, high-risk operations
- OpenAI uses HSTS and certificate transparency, making rogue certs more observable
WebSocket handshake with rogue certificate
wss:// connection, the attacker presents a self-signed or otherwise untrusted TLS certificate. Because WebSockex.start/4 is called with insecure: true and verify: :verify_none, the Erlang :ssl module accepts the certificate without checking the CA chain or hostname. The TLS handshake completes successfully with the attacker's proxy.- Attacker controls a TLS-terminating proxy presenting any certificate
- req_llm has not been patched to 1.29.0
- If the organization deploys TLS inspection / forward proxy with certificate pinning at the egress, the rogue cert may be detected or blocked
- Network IDS/IPS with JA3/JA4 fingerprinting may flag unusual TLS handshake patterns
:ssl debug logging shows the peer certificate details if enabledBearer token exfiltration and session hijack
authorization: Bearer <api_key_or_oauth_token> header from the WebSocket upgrade request, and proxies traffic onward to the real OpenAI endpoint. They can passively harvest the API key and all prompt/response content, or actively modify model responses before forwarding them to the victim application. For agentic workflows (Jido ecosystem), tampered responses could cause the agent to execute attacker-chosen actions.- Successful MitM established in step 3
- OpenAI API key usage anomalies (concurrent sessions from different IPs) may trigger rate limits or abuse detection on OpenAI's side
- Organizations using OAuth with short-lived tokens limit the window of exploitation
mix deps.update req_llm and verify mix.lock pins ≥1.29.0. The fix in PR #1140 supplies verified TLS options to WebSockex, enabling peer verification and hostname checking. Per the noisgate mitigation SLA for HIGH, deploy within 30 days. Given the one-day-old disclosure and no exploitation evidence, a measured rollout within 1-2 weeks is prudent.provider_options: [openai_stream_transport: :sse] (or simply remove any :websocket override, since SSE is the default). This routes traffic through Finch, which verifies TLS certificates. This does NOT help Realtime API users — ReqLLM.Providers.OpenAI.Realtime.connect/2 is WebSocket-only. Deploy within 30 days per the noisgate mitigation SLA for HIGH, or immediately if you cannot upgrade promptly.ReqLLM.Providers.OpenAI.Realtime.connect/2 and cannot upgrade immediately, disable Realtime functionality until the upgrade lands. There is no SSE alternative for this endpoint. Accept the feature regression as a temporary trade-off. Deploy within 30 days per the noisgate mitigation SLA.- Passing
ssl_optionsorcacertsto req_llm — the option allowlist in the vulnerable versions actively strips these parameters before they reach WebSockex, so caller-supplied TLS configuration is silently dropped. - WAF or API gateway in front of your application — the vulnerable connection is *outbound* from your app server to OpenAI, not inbound from clients. A WAF protecting your ingress has no visibility into this path.
- Upgrading only WebSockex to 0.6.0 — while WebSockex 0.6.0 defaults to
insecure: false, req_llm <1.29.0 does not pass the correct options to leverage this change. Both libraries need to be at their fixed versions, and the req_llm 1.29.0 fix is self-contained regardless of WebSockex version.
The supporting signals.
| In-the-Wild Exploitation | No known exploitation. Not listed in CISA KEV. No campaigns or threat actor references found as of 2026-10-24. Disclosed just one day ago. |
|---|---|
| Proof-of-Concept | No public PoC. No repos on GitHub named after CVE-2026-107685. No entries on pocindex.io, ExploitDB, or nuclei templates. The exploit is conceptually trivial (standard MitM proxy like mitmproxy suffices since the client accepts any cert), but no weaponized tooling exists. |
| EPSS Score | Not yet scored. CVE was disclosed 2026-10-23; EPSS data typically populates within 1-2 weeks of NVD publication. Expected to be low given on-path requirement and niche ecosystem. |
| KEV Status | Not listed. No CISA KEV entry. |
| CVSS Vector | GHSA/CNA CVSS 3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N = 7.4 HIGH. CVSS 4.0 (EEF CNA): CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N = 9.1 CRITICAL. The AC:H / AT:P correctly captures the on-path requirement. No availability impact. |
| Affected Versions | req_llm ≥ 1.9.0, < 1.29.0 (20 minor versions across ~9 months). The WebSocket streaming feature was introduced in 1.9.0. Versions < 1.9.0 do not have the WebSocket client and are unaffected. |
| Fixed Version | req_llm 1.29.0 (released 2026-10-22). The fix PR #1140 passes verified TLS options to WebSockex. Upstream WebSockex 0.6.0 also flips the default to insecure: false, but req_llm 1.29.0 does not require it — it supplies its own TLS options. |
| Installed Base / Exposure | 766K all-time hex.pm downloads, 54 dependents. ~225 downloads/day. Elixir is a niche ecosystem; estimated hundreds of production deployments. Only those using WebSocket transport (non-default) or the Realtime API are affected. No Shodan/Censys/GreyNoise signatures apply — this is a client-side library, not an internet-facing service. |
| Disclosure Timeline | 2026-10-22: Fix PR #1140 merged and 1.29.0 released. 2026-10-23: GHSA GHSA-6vww-5m28-f26v published. Coordinated by Jonatan Männchen / EEF Security WG. |
| Credit | Neil Berkman (finder, using Claude Mythos), Mike Hostetler (remediation developer), Jonatan Männchen / EEF (coordinator) |
Sources.
Why this verdict
- On-path requirement is real but not disqualifying: CVSS AC:H / AT:P correctly encodes this. In cloud VPCs the bar is high (DNS poisoning, BGP hijack, compromised router). In shared-network or dev/staging environments it drops to commodity tooling (
mitmproxy,bettercap). This is the single largest friction point and prevents this from being CRITICAL. - Default transport is SSE — safe: The
openai_stream_transportoption defaults to:sse, which uses Finch with full TLS verification. Only applications that *explicitly* set:websocketor call the Realtime API hit the vulnerable code path. This substantially narrows the exposed population, likely to a single-digit percentage of req_llm users. - Impact when conditions are met is genuinely HIGH: The attacker gets the OpenAI API key or OAuth bearer token in cleartext, full read/write on all prompt and response content, and can tamper with model outputs. For agentic AI systems (Jido ecosystem), manipulated responses could drive autonomous agents to execute attacker-chosen actions — this is an integrity impact that extends beyond the immediate session.
- Cannot be self-mitigated by the caller: The option allowlist strips
cacertsandssl_options, so a developer cannot enable verification without upgrading. This eliminates the usual 'configure it properly' mitigation path and increases urgency to patch. - Role multiplier: req_llm is an application-level library, not infrastructure software. It does not run on domain controllers, hypervisors, network edge devices, or other canonically high-value-role components. The blast radius is application-scoped: stolen API keys enable financial abuse and data exfiltration of LLM interactions, and response tampering affects the consuming application's logic. In agentic AI deployments (the Jido ecosystem), this *approaches* supply-chain-like impact because tampered LLM outputs drive autonomous actions — but req_llm is not canonically an infrastructure component, and the on-path prerequisite compounds downward. The floor for HIGH is met but CRITICAL is not warranted.
Why not higher?
CRITICAL would require either a broader reachable population or a lower attacker bar. The on-path requirement is genuine friction in production cloud environments, the default SSE transport means most installations are unexposed, and the Elixir ecosystem is niche (~766K total downloads, estimated hundreds of production deployments). There is no evidence of active exploitation, no public PoC, and the library is not canonically infrastructure software where fleet-scale compromise follows automatically.
Why not lower?
MEDIUM would understate the impact for the subset of deployments that ARE exposed. When the WebSocket path is active, the attacker gets the API key or OAuth token with zero user interaction and zero authentication — that is full credential theft. The application developer cannot self-mitigate (the option allowlist blocks it), leaving upgrade as the only path. The Realtime API has no SSE fallback, so those users have no workaround at all. The integrity impact in agentic AI systems (response tampering driving autonomous actions) elevates this above a theoretical-only concern.
Crowdsourced verification payload.
Run this on any host or CI runner where an Elixir project is built or deployed. Point it at the project root containing mix.lock. No special privileges needed — just read access to the lockfile. Example: bash check_cve_2026_107685.sh /opt/myapp
#!/usr/bin/env bash
# CVE-2026-107685 Checker — agentjido req_llm WebSocket TLS bypass
# Checks mix.lock for a vulnerable req_llm version (>= 1.9.0, < 1.29.0).
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
LOCKFILE="$PROJECT_DIR/mix.lock"
VULN_FIRST="1.9.0"
FIXED="1.29.0"
if [[ ! -f "$LOCKFILE" ]]; then
echo "UNKNOWN — mix.lock not found at $LOCKFILE"
exit 2
fi
# Extract req_llm version from mix.lock
# Typical line: "req_llm": {:hex, :req_llm, "1.22.0", ...
VERSION=$(grep -oP '"req_llm":\s*\{:hex,\s*:req_llm,\s*"\K[^"]+' "$LOCKFILE" 2>/dev/null || true)
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN — req_llm not found in $LOCKFILE (not a dependency of this project)"
exit 2
fi
# version_gte: returns 0 if $1 >= $2
version_gte() {
[[ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" == "$2" ]]
}
# version_lt: returns 0 if $1 < $2
version_lt() {
[[ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" == "$1" ]] && [[ "$1" != "$2" ]]
}
if version_gte "$VERSION" "$FIXED"; then
echo "PATCHED — req_llm $VERSION >= $FIXED (CVE-2026-107685)"
exit 0
elif version_gte "$VERSION" "$VULN_FIRST" && version_lt "$VERSION" "$FIXED"; then
echo "VULNERABLE — req_llm $VERSION is in affected range [$VULN_FIRST, $FIXED) — CVE-2026-107685"
# Additional check: see if WebSocket transport is used in source
if grep -rq 'openai_stream_transport.*:websocket\|Realtime\.connect' "$PROJECT_DIR/lib" 2>/dev/null; then
echo " WARNING: WebSocket transport usage detected in source — this deployment IS exposed."
else
echo " NOTE: No WebSocket transport usage found in lib/ — default SSE transport may not be affected."
echo " Verify manually; the library is still vulnerable if WebSocket is used at runtime."
fi
exit 1
else
echo "PATCHED — req_llm $VERSION < $VULN_FIRST (predates vulnerable WebSocket client)"
exit 0
fi