Like a fire alarm that only works if you installed an optional upgrade nobody ordered
CVE-2026-97031 is a denial-of-service flaw in Go's crypto/tls standard library. RFC 9849 prohibits a TLS ClientHello from referencing the same Encrypted Client Hello (ECH) outer extension more than once, but Go's server-side implementation accepted repeated references without validation. An attacker can send a single crafted ClientHello with many duplicated ECH outer extension pointers, causing the server to allocate unbounded memory during the handshake. The bug exists in Go versions from 1.24 (when ECH server support was introduced) through 1.26.8, and in 1.27.0–1.27.1. Fixed in Go 1.26.9 and 1.27.2. Affected symbols include Conn.Handshake, Conn.HandshakeContext, Conn.Read, Conn.Write, Dial, and QUICConn.Start.
The vendor's HIGH / 7.5 rating is technically correct for the narrow population of Go TLS servers that have ECH explicitly enabled via Config.EncryptedClientHelloKeys. However, it dramatically overstates real-world risk for the vast majority of Go deployments. ECH server-side support is opt-in, requires explicit key provisioning and DNS HTTPS record publication, and is so uncommon that KrakenD — a major Go-based API gateway — flagged this CVE as a *false positive* because they never configure ECH keys. Traefik's ECH server support is still an unmerged pull request. Caddy supports it but requires a manual ech directive. Real-world server-side ECH deployment remains below 10% of the top million sites (dominated 99.99% by Cloudflare, which isn't Go-based), and virtually zero internal enterprise Go services have ECH enabled. The 7.5 score treats every Go TLS listener as exposed; in practice, almost none are.
4 steps from start to impact.
Identify a Go TLS server with ECH enabled
crypto/tls that has populated Config.EncryptedClientHelloKeys. ECH advertisement is discoverable via DNS HTTPS resource records (SVCB/HTTPS RR type 65) which publish the server's ECH config list. The attacker can enumerate targets by querying DNS for HTTPS records and checking for ech= parameters, then fingerprinting the TLS stack to confirm Go.- Target must be a Go application using crypto/tls for TLS termination
- Target must have ECH server support explicitly configured with EncryptedClientHelloKeys
- ECH server config is opt-in and requires key generation, tls.Config population, and DNS record publication
- KrakenD, Traefik (unmerged PR), and most Go frameworks do not enable ECH by default
- Server-side ECH adoption is <10% of top-million sites, 99.99% of which are Cloudflare (not Go-based)
- Enterprise internal Go services have effectively zero ECH adoption
ech= parameter is observable via DNS query logging. Shodan and Censys can fingerprint Go TLS servers but cannot confirm ECH enablement without active TLS probing.Craft a malicious ClientHello with repeated ECH outer extension references
- Knowledge of the ECH ClientHello wire format (RFC 9849)
- Ability to send raw TLS packets to the target port
- No public PoC or weaponized tool exists as of 2026-10-24
- Requires TLS protocol-level crafting, not a simple HTTP request
Trigger server-side memory amplification during TLS handshake
Conn.Handshake processes the malicious ClientHello. The ECH decryption path iterates over the repeated outer extension references and allocates memory for each copy without deduplication or bounds checking. A single crafted packet can amplify server memory consumption by orders of magnitude relative to the packet size — a classic asymmetric resource consumption (CWE-405) pattern. The amplification factor depends on the number of repeated references and the sizes of the referenced extensions.- Server must accept and begin processing the TLS handshake
- No upstream TLS termination proxy intercepting the connection first
- Rate limiting on TLS handshakes per source IP at the load balancer would slow sustained exploitation
- Go's garbage collector may partially reclaim short-lived allocations, but peak allocation during handshake processing can still trigger OOM
go_memstats_alloc_bytes, container cgroup metrics, Datadog/New Relic Go runtime dashboards) would detect rapid memory growth correlated with handshake processing.Achieve denial of service via OOM or GC thrashing
- Sustained sending of crafted ClientHellos to maintain memory pressure
- Container memory limits (cgroup) bound the blast radius to the individual process/pod
- Kubernetes restart policies, systemd watchdogs, and process supervisors auto-recover crashed processes
- Load balancers distributing across multiple backends reduce single-target impact
- Source-IP rate limiting at the network edge constrains attack throughput
dmesg OOM killer entries on bare-metal hosts.tls.Config for EncryptedClientHelloKeys population. If absent (the default), the vulnerable code path is unreachable and no further action is needed for this CVE beyond documenting the finding. As a MEDIUM verdict, there is no mitigation SLA — go straight to the 365-day noisgate remediation window for the underlying Go runtime upgrade.EncryptedClientHelloKeys from your tls.Config and delete the corresponding DNS HTTPS records. This completely eliminates the attack vector with zero functionality impact for most services. No mitigation SLA applies at MEDIUM — deploy within the 365-day noisgate remediation window, though this is a trivial config change worth doing immediately.resources.limits.memory in Kubernetes, --memory in Docker) to bound the blast radius of memory exhaustion to a single pod/container. Ensure liveness probes or systemd watchdog restart the service on OOM. This converts a sustained outage into brief, bounded disruption (~10-30 second recovery). Deploy within the 365-day noisgate remediation window.rate-limit sessions, NGINX limit_conn, cloud LB connection rate policies) to cap new TLS connections per source IP per second. This raises the cost of sustained exploitation but does not fully prevent it — distributed sources bypass per-IP limits. Deploy within the 365-day noisgate remediation window.- Web Application Firewall (WAF) — WAFs operate at HTTP layer (L7) after the TLS handshake completes. This vulnerability is triggered *during* the TLS handshake at the ClientHello stage, before any HTTP traffic exists. The WAF never sees the malicious payload.
- Go
http.Servertimeouts (ReadTimeout,WriteTimeout,IdleTimeout) — These govern HTTP-level I/O timing, not TLS handshake memory allocation. The memory exhaustion occurs insidecrypto/tls.Conn.Handshakebefore the HTTP server layer is reached. - IP reputation blocklists — A DoS attacker can trivially rotate source IPs via botnets or cloud instances. Reactive IP blocking does not prevent the initial memory spike from each new source address.
- Disabling TLS 1.3 — ECH is a TLS 1.3 extension, so disabling TLS 1.3 would technically prevent the attack, but this is a catastrophically disproportionate measure that breaks modern client compatibility, disables post-handshake security improvements, and introduces far worse security regressions than the DoS it prevents.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed on CISA KEV. No reports from GreyNoise, Shadowserver, or threat intelligence feeds as of 2026-10-24. No known campaigns targeting this vulnerability. The narrow ECH precondition makes mass exploitation uneconomical. |
|---|---|
| Proof of Concept | No public PoC found. pocindex.io returned no results for this CVE. No GitHub repositories named after CVE-2026-97031 contain exploit code. The Go issue #81855 does not include a reproduction script. Crafting a PoC requires ECH wire-format knowledge (RFC 9849) and a custom TLS ClientHello builder — straightforward for a TLS researcher but not script-kiddie accessible. |
| EPSS | 0.00394 (0.394%) — very low exploitation probability, well below the 10th percentile of scored CVEs. Consistent with no observed exploitation and the narrow ECH-enablement precondition. |
| CISA KEV Status | Not listed as of 2026-10-24. No due date. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5 HIGH. Network-reachable, no auth, no interaction, availability-only. The AC:L rating is debatable — ECH enablement is a deployment precondition beyond attacker control, which CVSS 3.1 §2.1.2 suggests warrants AC:H (yielding 5.9). Vendor chose AC:L assuming ECH is active. |
| Affected Versions | Go 1.24.0 through 1.26.8 (ECH server support introduced in Go 1.24), and Go 1.27.0–1.27.1. Only the 1.26.x and 1.27.x branches received patches per Go's two-release support policy. Go 1.24.x and 1.25.x are EOL and remain permanently vulnerable. |
| Fixed Versions | Go 1.26.9 and 1.27.2. Commits: master 3ccab63, 1.27 backport f022e61, 1.26 backport eb1bf6e. Distro packages: RHEL go-toolset-1.26.10-1.el10_2 (RHSA-2026:86093), RHEL 9 (RHSA-2026:86196), Debian tracked via DEBIAN-CVE-2026-97031, AlmaLinux and Wolfi also shipping fixes. |
| ECH Server Adoption | Extremely low. <10% of top-million sites offer ECH server-side; 99.99% of those are Cloudflare (not Go-based). KrakenD explicitly marked this a false positive — they never configure ECH keys. Traefik's ECH PR is unmerged. Caddy requires explicit ech directive. Internal enterprise Go services have effectively zero ECH adoption. |
| Disclosure Timeline | Issue opened 2026-09-29, fix committed and issue closed 2026-10-08, public advisory via GO-2026-6607 on 2026-10-08. Assigned to thatnealpatel (Go security team). RHSA advisories published within the following week. |
| Reporter / Credit | Not publicly attributed. Filed via Go security team internal process. Go issue #81855 was opened by the Go team as a security-fix tracking issue, classified as release-blocker. |
Sources.
- Go Vulnerability Database — GO-2026-6607
- Go Issue #81855 — crypto/tls: reject malformed ECH outer extension references
- Red Hat CVE-2026-97031
- KrakenD Security Advisory — CVE-2026-97031 (false positive analysis)
- OSV — DEBIAN-CVE-2026-97031
- Go CL 847312 — Fix commit
- Traefik ECH PR #13758 (unmerged — evidence of low adoption)
- RHSA-2026:86093 — go-toolset security update for RHEL
Why this verdict
- ECH opt-in prerequisite eliminates >99% of the attack surface. ECH server support requires explicit
Config.EncryptedClientHelloKeyspopulation, key generation, and DNS HTTPS record publication. No major Go framework, API gateway, or infrastructure tool enables ECH by default. KrakenD — a production API gateway used at scale — flagged this CVE as a *false positive* because they never configure ECH keys. Traefik's ECH support hasn't even been merged. This is a deployment precondition beyond attacker control that CVSS 3.1 should model as AC:H. This single friction point accounts for approximately -1.5 points from the vendor baseline. - DoS-only impact with no persistence, escalation, or data compromise. The attack yields temporary availability loss — no code execution, no credential theft, no lateral movement, no data exfiltration. Once malicious traffic stops, the service recovers. Container orchestrators and process supervisors auto-restart OOM-killed processes within seconds. In modern Kubernetes environments, this is a brief blip, not a sustained outage. This caps the maximum severity well below RCE-class vulnerabilities. Adjustment: -0.4 from baseline.
- Zero exploitation evidence and near-zero tooling. EPSS is 0.00394, no CISA KEV listing, no public PoC, no observed campaigns, no scanner detections. The narrow ECH precondition makes this CVE uneconomical for threat actors compared to broadly exploitable DoS vectors (e.g., HTTP/2 rapid reset). Adjustment: -0.3 from baseline.
- Role multiplier: Go's
crypto/tlsis embedded in canonically high-value infrastructure — Kubernetes API server, etcd, HashiCorp Vault, containerd, Caddy, Traefik, Docker. However, the ECH prerequisite effectively nullifies the high-value-role floor for this CVE. (1) kube-apiserver, etcd, Vault, containerd, Docker: Do not support or configure ECH — chain does not succeed. (2) Caddy: Supports ECH but requires manualechdirective — chain succeeds only in the small fraction of Caddy instances with ECH enabled; blast radius is availability-only for that reverse proxy instance, not domain/fleet compromise. (3) Traefik: ECH support is an unmerged PR — chain cannot succeed. The fraction of high-value-role installs where the chain actually succeeds is well below 1% of the Go crypto/tls installed base. Therefore the HIGH floor does not engage. Even where the chain does succeed, the outcome is bounded DoS (host-level, recoverable), not domain takeover, fleet compromise, or supply-chain pivot. - Net adjustment: vendor 7.5 → reassessed 5.3 (MEDIUM). The 2.2-point reduction is driven almost entirely by the ECH opt-in prerequisite (-1.5), with smaller reductions for DoS-only impact in orchestrated environments (-0.4) and absent exploitation evidence (-0.3). The score remains above LOW because the attack *is* trivially exploitable for the rare server that has ECH enabled.
Why not higher?
A HIGH rating would require the vulnerable code path to be reachable in a substantial fraction of deployments, or for the impact to extend beyond availability. Neither condition holds. ECH server enablement is an explicit, rare configuration choice — not a default. The impact is bounded to temporary service unavailability with no confidentiality or integrity consequences. Even in the worst realistic case (a public-facing Caddy instance with ECH explicitly configured), the outcome is a restartable process-level DoS, not domain compromise, fleet-scale impact, or data breach. The installed-base share of Go TLS servers with ECH enabled is conservatively below 0.5%.
Why not lower?
A LOW or IGNORE rating would understate the risk for the small but real population of Go servers that *have* enabled ECH. For those deployments, the attack is unauthenticated, network-reachable, requires no user interaction, and can be triggered with a single crafted packet per handshake. The asymmetric amplification factor means even a low-bandwidth attacker can OOM a server process. The vulnerability is trivially exploitable once the ECH precondition is met, and the fix requires a full Go runtime rebuild — not a simple configuration toggle. The Go standard library's ubiquity means even a small percentage of affected installs represents a non-trivial absolute number of services.
Crowdsourced verification payload.
Run on each target host or in CI to scan Go binaries for vulnerable runtime versions. Execute as any user with read access to binary paths: bash cve-2026-97031-check.sh. No root required unless scanning privileged paths. Scans /usr/local/bin, /usr/bin, /opt, and /srv by default.
#!/usr/bin/env bash
# CVE-2026-97031 — Go crypto/tls ECH Memory Exhaustion Checker
# Scans Go toolchain and compiled binaries for vulnerable versions.
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -uo pipefail
VULN=0; CHECKED=0; OK=0
is_vulnerable() {
local v="${1#go}"
local maj min pat
IFS='.' read -r maj min pat <<< "$v"
pat="${pat:-0}"
[[ "$maj" -ne 1 ]] && return 2
# ECH server support added in Go 1.24; earlier versions not affected
[[ "$min" -lt 24 ]] && return 1
# 1.24.x-1.25.x: vulnerable, EOL, no fix backported
[[ "$min" -lt 26 ]] && return 0
# 1.26.x: fixed in 1.26.9
if [[ "$min" -eq 26 ]]; then
[[ "$pat" -lt 9 ]] && return 0 || return 1
fi
# 1.27.x: fixed in 1.27.2
if [[ "$min" -eq 27 ]]; then
[[ "$pat" -lt 2 ]] && return 0 || return 1
fi
# 1.28+: patched
return 1
}
check_ver() {
local label="$1" ver="$2"
CHECKED=$((CHECKED+1))
if is_vulnerable "$ver"; then
echo "[!] VULNERABLE: $label ($ver)"
VULN=$((VULN+1))
else
echo "[+] PATCHED: $label ($ver)"
OK=$((OK+1))
fi
}
echo "=== CVE-2026-97031 Scanner ==="
echo "Go crypto/tls ECH outer extension memory exhaustion"
echo "Vuln range: go1.24.0 - go1.26.8, go1.27.0 - go1.27.1"
echo "Fixed in: go1.26.9, go1.27.2"
echo ""
# 1. Check Go toolchain
if command -v go &>/dev/null; then
GV=$(go version 2>/dev/null | awk '{print $3}')
check_ver "go toolchain" "$GV"
else
echo "[i] No Go toolchain in PATH"
fi
# 2. Scan Go binaries
SCAN_DIRS="/usr/local/bin /usr/bin /opt /srv"
echo ""
echo "Scanning Go binaries in: $SCAN_DIRS"
for d in $SCAN_DIRS; do
[[ -d "$d" ]] || continue
while IFS= read -r -d '' bin; do
BV=""
if command -v go &>/dev/null; then
BV=$(go version "$bin" 2>/dev/null | awk '{print $2}') || true
fi
if [[ -z "$BV" ]]; then
BV=$(strings "$bin" 2>/dev/null | grep -oP 'go1\.\d+\.\d+' | head -1) || true
fi
[[ -z "$BV" ]] && continue
check_ver "$bin" "$BV"
done < <(find "$d" -maxdepth 3 -type f -executable -print0 2>/dev/null)
done
# 3. Check running Go processes
echo ""
echo "Checking running processes..."
for pid in /proc/[0-9]*/exe; do
exe=$(readlink "$pid" 2>/dev/null) || continue
[[ -f "$exe" ]] || continue
RV=$(strings "$exe" 2>/dev/null | grep -oP 'go1\.\d+\.\d+' | head -1) || true
[[ -z "$RV" ]] && continue
pname=$(basename "$exe")
pidnum=$(echo "$pid" | grep -oP '\d+')
# Deduplicate: only check each binary once
check_ver "PID $pidnum ($pname)" "$RV"
done 2>/dev/null
echo ""
echo "=== Result ==="
echo "Checked: $CHECKED | Vulnerable: $VULN | Patched: $OK"
if [[ "$CHECKED" -eq 0 ]]; then
echo "UNKNOWN — no Go binaries found"
exit 2
elif [[ "$VULN" -gt 0 ]]; then
echo "VULNERABLE — $VULN binary(ies) on affected Go runtime"
echo "NOTE: Only exploitable if ECH server is configured (EncryptedClientHelloKeys set)"
exit 1
else
echo "PATCHED — all $OK Go binary(ies) on fixed versions"
exit 0
fi