← Back to Feed CACHED · 2026-10-24 04:48:00 · CACHE_KEY CVE-2026-107685
CVE-2026-107685 · CWE-295 · Disclosed 2026-10-23

agentjido req_llm WebSocket streaming client improper TLS certificate validation

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"On-path attackers can steal OpenAI API keys from Elixir apps using req_llm WebSocket streaming — upgrade to 1.29.0"
02 · The Attack Path

4 steps from start to impact.

STEP 01

Victim app opts into WebSocket transport

An Elixir application depending on req_llm ≥1.9.0 <1.29.0 either sets 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.
Conditions required:
  • Application uses req_llm ≥1.9.0 <1.29.0
  • WebSocket transport is selected (explicit opt-in or Realtime API)
Where this breaks in practice:
  • Default transport is :sse via 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
Detection/coverage: Dependency audit tools (mix audit, Dependabot, Snyk) flag the GHSA advisory. Static analysis of mix.lock for version range match.
STEP 02

Attacker achieves on-path position

The attacker must position themselves on the network path between the application server and 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.
Conditions required:
  • Network-level access between the app server and OpenAI endpoints
  • Ability to intercept and proxy TCP connections on port 443
Where this breaks in practice:
  • 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
Detection/coverage: Network anomaly detection, certificate transparency log monitoring, GreyNoise/Shodan for unexpected TLS endpoints
STEP 03

WebSocket handshake with rogue certificate

When the app initiates a 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.
Conditions required:
  • Attacker controls a TLS-terminating proxy presenting any certificate
  • req_llm has not been patched to 1.29.0
Where this breaks in practice:
  • 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
Detection/coverage: TLS inspection appliances at network egress; Erlang :ssl debug logging shows the peer certificate details if enabled
STEP 04

Bearer token exfiltration and session hijack

The attacker now terminates TLS on their proxy, reads the 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.
Conditions required:
  • Successful MitM established in step 3
Where this breaks in practice:
  • 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
Detection/coverage: OpenAI usage dashboard anomalies; application-level logging of unexpected model outputs; network traffic analysis showing certificate mismatch
03 · Compensating Control

1
HIGH 6.5→IGNORE 0.0
SEVERITY REDUCED
Upgrade req_llm to ≥1.29.0 — This is the definitive fix. Run 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.
2
HIGH 6.5→LOW 2.0
SEVERITY REDUCED
Switch to SSE transport where possible — Set 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.
3
HIGH 6.5→IGNORE 0.0
SEVERITY REDUCED
Disable Realtime API calls on vulnerable versions — If your application uses 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.
4
HIGH 6.5→MEDIUM 5.0
SEVERITY REDUCED
Restrict API key scope and enable IP allowlisting — On the OpenAI platform, restrict the API key to only the IP ranges of your application servers and limit its permissions to the minimum required endpoints. This does not prevent interception but limits the attacker's ability to reuse a stolen key from a different network location. Useful as defense-in-depth.
5
HIGH 6.5→HIGH 6.0
Monitor OpenAI API usage for anomalies — Enable logging and alerting on the OpenAI usage dashboard for unexpected spikes, concurrent sessions from unknown IPs, or requests to endpoints your application does not use. This is detective, not preventive, but shortens dwell time if a key is compromised.
What doesn't work
  • Passing ssl_options or cacerts to 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.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNo 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-ConceptNo 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 ScoreNot 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 StatusNot listed. No CISA KEV entry.
CVSS VectorGHSA/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 Versionsreq_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 Versionreq_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 / Exposure766K 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 Timeline2026-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.
CreditNeil Berkman (finder, using Claude Mythos), Mike Hostetler (remediation developer), Jonatan Männchen / EEF (coordinator)

Sources.

  1. GHSA Advisory (GHSA-6vww-5m28-f26v)
  2. OSV Entry (EEF-CVE-2026-107685)
  3. Fix PR #1140 — verify TLS for HTTPS WebSocket URLs
  4. WebSockex PR #144 — verify TLS peers by default
  5. req_llm on hex.pm
  6. Strix.ai CVE Analysis
  7. HOL Guard Advisory
  8. EEF Security WG — Erlang/Elixir TLS hardening guide
05 · The Call

Final Verdict
= UNCHANGED to HIGH (6.5/10)

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_transport option defaults to :sse, which uses Finch with full TLS verification. Only applications that *explicitly* set :websocket or 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 cacerts and ssl_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.

06 · Verification

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

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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