← Back to Feed CACHED · 2026-10-24 12:56:21 · CACHE_KEY CVE-2026-97031
CVE-2026-97031 · CWE-405 · Disclosed 2026-10-08

Go crypto/tls server-side memory exhaustion via repeated ECH outer extension references

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

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.

"ECH opt-in requirement shrinks this Go DoS from 7.5 to a narrow-exposure MEDIUM."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify a Go TLS server with ECH enabled

The attacker needs to find a server running Go's 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.
Conditions required:
  • Target must be a Go application using crypto/tls for TLS termination
  • Target must have ECH server support explicitly configured with EncryptedClientHelloKeys
Where this breaks in practice:
  • 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
Detection/coverage: DNS enumeration of HTTPS RRs with 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.
STEP 02

Craft a malicious ClientHello with repeated ECH outer extension references

The attacker constructs a TLS 1.3 ClientHello containing an ECH extension with many duplicated outer extension reference indices. RFC 9849 §5.1 forbids this, but unpatched Go servers do not validate uniqueness. Each repeated reference causes the server to re-copy the referenced extension data during ECH decryption, amplifying memory usage proportional to the repetition count. No weaponized tool or public PoC exists as of this assessment, but writing one requires only standard ECH wire-format knowledge from the RFC.
Conditions required:
  • Knowledge of the ECH ClientHello wire format (RFC 9849)
  • Ability to send raw TLS packets to the target port
Where this breaks in practice:
  • No public PoC or weaponized tool exists as of 2026-10-24
  • Requires TLS protocol-level crafting, not a simple HTTP request
Detection/coverage: TLS inspection appliances parsing ClientHello extensions could flag abnormally large ECH payloads or repeated outer extension indices, but most enterprise TLS inspection does not parse ECH at this depth.
STEP 03

Trigger server-side memory amplification during TLS handshake

The server's 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.
Conditions required:
  • Server must accept and begin processing the TLS handshake
  • No upstream TLS termination proxy intercepting the connection first
Where this breaks in practice:
  • 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
Detection/coverage: Process-level memory monitoring (Prometheus go_memstats_alloc_bytes, container cgroup metrics, Datadog/New Relic Go runtime dashboards) would detect rapid memory growth correlated with handshake processing.
STEP 04

Achieve denial of service via OOM or GC thrashing

Repeated malicious handshakes exhaust the server process's available memory, causing either an out-of-memory kill by the OS or container runtime, or severe GC thrashing that renders the service unresponsive. Impact is strictly availability — no code execution, no data exfiltration, no privilege escalation. The attack requires sustained traffic; once malicious packets stop, the process can recover or be restarted by orchestration. Kubernetes pods with liveness probes will restart automatically, limiting outage duration.
Conditions required:
  • Sustained sending of crafted ClientHellos to maintain memory pressure
Where this breaks in practice:
  • 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
Detection/coverage: Container OOM kills logged by kubelet and Docker daemon. Process crash events in systemd journal. Rapid TLS handshake failure spikes in APM dashboards. dmesg OOM killer entries on bare-metal hosts.
03 · Compensating Control

1
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Confirm ECH is not configured; if unused, document and close — The overwhelming majority of Go TLS servers do not configure ECH. Audit your application's 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.
2
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Disable ECH server support if not business-critical — If ECH is configured but not operationally required (e.g., enabled experimentally or in dev), remove the 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.
3
MEDIUM 5.3→LOW 3.1
SEVERITY REDUCED
Enforce container memory limits and auto-restart policies — If ECH must remain enabled pending the Go upgrade, set cgroup memory limits (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.
4
MEDIUM 5.3→MEDIUM 4.3
Rate-limit new TLS handshakes per source IP at the load balancer — Configure your load balancer or edge proxy (HAProxy 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.
5
MEDIUM 5.3→IGNORE 0.0
SEVERITY REDUCED
Upgrade Go runtime to 1.26.9+ or 1.27.2+ and rebuild all binaries — The definitive remediation. Rebuild and redeploy all Go binaries using Go ≥1.26.9 or ≥1.27.2. The patch in CL 847312 rejects malformed ClientHellos with repeated ECH outer extension references before memory allocation occurs. Complete within the 365-day noisgate remediation SLA for MEDIUM severity.
What doesn't work
  • 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.Server timeouts (ReadTimeout, WriteTimeout, IdleTimeout) — These govern HTTP-level I/O timing, not TLS handshake memory allocation. The memory exhaustion occurs inside crypto/tls.Conn.Handshake before 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.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNone 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 ConceptNo 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.
EPSS0.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 StatusNot listed as of 2026-10-24. No due date.
CVSS VectorCVSS: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 VersionsGo 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 VersionsGo 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 AdoptionExtremely 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 TimelineIssue 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 / CreditNot 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.

  1. Go Vulnerability Database — GO-2026-6607
  2. Go Issue #81855 — crypto/tls: reject malformed ECH outer extension references
  3. Red Hat CVE-2026-97031
  4. KrakenD Security Advisory — CVE-2026-97031 (false positive analysis)
  5. OSV — DEBIAN-CVE-2026-97031
  6. Go CL 847312 — Fix commit
  7. Traefik ECH PR #13758 (unmerged — evidence of low adoption)
  8. RHSA-2026:86093 — go-toolset security update for RHEL
05 · The Call

Final Verdict
↓ DOWNGRADED to MEDIUM (5.3/10)

Why this verdict

  • ECH opt-in prerequisite eliminates >99% of the attack surface. ECH server support requires explicit Config.EncryptedClientHelloKeys population, 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/tls is 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 manual ech directive — 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.

06 · Verification

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.

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