← Back to Feed CACHED · 2026-06-21 16:21:38 · CACHE_KEY CVE-2026-35469
CVE-2026-35469 · CWE-770 · Disclosed 2026-04-16

spdystream is a Go library for multiplexing streams over SPDY connections

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

A zip-bomb stuffed into a SPDY header that makes your kubelet eat itself

spdystream is the Go library moby/spdystream, used inside the Kubernetes / container ecosystem to multiplex streams for things like kubectl exec, kubectl port-forward, kubectl attach, and CRI streaming. Versions ≤ 0.5.0 read three attacker-controlled 32-bit fields directly out of a SPDY/3 control frame — the SETTINGS entry count, the header count in parseHeaderValueBlock, and per-header field lengths — and pass each one straight into a Go allocation without bounds checking. Because SPDY header blocks are zlib-compressed, a few hundred bytes on the wire decompresses into gigabytes of demanded memory, so a single crafted frame causes the receiving process to OOM-kill. Fixed in 0.5.1.

There is no vendor CVSS to compare against, but third parties (SentinelOne, OffSeq) have floated 8.7 HIGH. That is too hot. The bug is a deterministic DoS — no RCE, no info-disclosure, no memory corruption — and the realistic exposure is kubelet's streaming endpoint, which sits behind webhook/TLS-client auth in any reasonable cluster. The pattern matters (every node, every pod-exec path) but the impact ceiling is *process crash and restart*, not cluster takeover.

"Unauthenticated memory bomb against SPDY parsers — but the only ear listening is your kubelet, and it wants a client cert first."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify a service running spdystream

Attacker enumerates a target that links github.com/moby/spdystream — overwhelmingly this is kubelet's streaming server (port 10250) or a CRI shim, occasionally Cilium / Hubble / older Docker daemon paths. Tooling: kube-hunter, nuclei kubelet templates, Shodan dork product:"kubelet".
Conditions required:
  • Network reachability to a kubelet streaming endpoint or other spdystream consumer
  • Target version of spdystream ≤ 0.5.0 still resident in the binary
Where this breaks in practice:
  • Kubelet read-only port 10255 is disabled by default since 1.10
  • Hardened clusters require client-cert / webhook auth on 10250
Detection/coverage: Shodan/Censys can fingerprint exposed kubelets; most enterprise scanners flag exposed 10250 as a finding already
STEP 02

Reach the SPDY upgrade handler

Attacker needs to drive the target into the SPDY protocol path — usually by hitting /exec, /attach, or /portForward on kubelet and negotiating the SPDY/3.1 upgrade. Tooling: a hand-rolled Go client built on moby/spdystream, or a forked kubectl with a tampered transport.
Conditions required:
  • Ability to complete TLS handshake (and present a client cert if required)
  • Endpoint actually invokes the spdystream frame parser before authz on the streamed action
Where this breaks in practice:
  • Kubelet authn (--anonymous-auth=false) blocks unauthenticated callers before the SPDY upgrade in most managed-Kubernetes distros (EKS/AKS/GKE)
  • Service meshes and admission policies typically deny non-API-server traffic to 10250
Detection/coverage: Audit logs on kube-apiserver will not see this — it is node-direct; node-level network policies are the only signal
STEP 03

Send the crafted SETTINGS / SYN_STREAM frame

Attacker sends a single SPDY control frame whose zlib-compressed header block decompresses into a SETTINGS entry count near 2^31, or a header count / per-header length in the same range. The vulnerable parser calls make([]Setting, n) or equivalent with the attacker's value. PoC pattern: small zlib input → multi-gigabyte allocation. No public weaponized PoC is on GitHub at the time of writing, but the bug class is trivially reproducible from the patch diff.
Conditions required:
  • A live SPDY session post-upgrade
  • Library code path that has not been wrapped by a higher-layer size cap
Where this breaks in practice:
  • Per-container cgroup memory limits cap blast radius to the kubelet process, which systemd restarts
  • Linux OOM killer typically picks the kubelet before the host panics
Detection/coverage: EDR will not catch the frame itself; node exporter / node_memory_MemAvailable_bytes collapse is the post-hoc signal
STEP 04

Loop for sustained node DoS

A single frame crashes the kubelet once. To create a real outage the attacker scripts re-connection and re-send, denying pod scheduling and kubectl exec cluster-wide. On nodes without Restart=always or with crashloop backoff tuned aggressively, this manifests as NodeNotReady and pod evictions.
Conditions required:
  • Persistent reachability
  • No rate-limit or IP block between attacker and kubelet
Where this breaks in practice:
  • Any halfway-tuned NetworkPolicy / security group blocks east-west traffic to 10250
  • Node auto-repair (GKE/EKS) will replace the node before sustained impact
Detection/coverage: Prometheus up{job="kubelet"} == 0 and control-plane node-status churn are loud
03 · Intelligence Metadata

The supporting signals.

In the wildNo known exploitation. Not on CISA KEV. No GreyNoise tag as of 2026-06-22.
Public PoCNo weaponized PoC published. Patch diff in moby/spdystream PR for 0.5.1 is sufficient to reconstruct one in <1 hour.
EPSS0.0043 (~0.43%) — bottom-quartile exploitation probability.
KEVNot listed.
CVSSNo NVD score. Third-party (SentinelOne) floats 8.7 / HIGH — noisgate disagrees, see The Call.
Affectedgithub.com/moby/spdystream ≤ 0.5.0. Transitively reaches anything pulling k8s.io/apimachinery/pkg/util/httpstream/spdy or older docker/spdystream forks.
Fixed0.5.1. Kubernetes will pick this up in subsequent kubelet patch releases; check your distro vendor (RHEL/SUSE/Chainguard) for backports.
ExposureShodan shows ~5–8k internet-facing kubelets on 10250 globally — most respond 401 Unauthorized to anonymous calls.
Disclosed2026-04-16 via the Kubernetes / moby coordinated advisory.
ReporterResearcher credit per the moby/spdystream GHSA; reported through the Kubernetes Security Response Committee.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (5.3/10)

This is a deterministic memory-exhaustion DoS with no RCE, no integrity impact, and no path to credential or data exposure — the impact ceiling is a kubelet OOM and node restart. The single most decisive factor is that the only realistic listener is kubelet's authenticated streaming endpoint, which rejects anonymous callers by default in every modern Kubernetes distribution.

HIGH DoS-only impact bound (no memory corruption primitive)
HIGH Fixed version and patch scope
MEDIUM Internet exposure population (depends on cluster hardening)
LOW Public exploitation timing — diff-to-exploit is easy

Why this verdict

  • Impact ceiling is DoS. CWE-770 with no follow-on primitive — process crashes, systemd restarts kubelet, no code execution, no data exposure.
  • Authn gate stands in front. Kubelet's --anonymous-auth=false (default on EKS, AKS, GKE, OpenShift, kubeadm ≥1.10) means an unauthenticated network attacker hits 401 before the SPDY parser is reached.
  • Blast radius is one node. Even on success, cgroup limits and node auto-repair contain the outage to a single kubelet; no lateral, no cluster-wide collapse from one frame.
  • Role multiplier: kubelet (typical role). Kubelet runs on every node and is canonically a high-value component, BUT the chain here ends in *node restart*, not domain/cluster takeover. No identity, secrets, or control-plane compromise flows from this bug. Floor stays at MEDIUM.
  • Role multiplier: exposed CRI/Docker shim (rare). If a legacy docker/spdystream-backed daemon is exposed without auth (very uncommon in 2026), same DoS, same single-host blast radius.
  • No KEV, EPSS 0.43%, no public weaponization. Threat tempo does not justify upgrading above MEDIUM.

Why not higher?

HIGH would require either a meaningful path to RCE / privilege escalation (there is none — Go's allocator faults cleanly), or that the typical deployment exposes spdystream to the open internet without auth (it does not — kubelet authn is on by default and the bug sits behind it). A node-restart DoS on hardened, authenticated endpoints does not clear the HIGH bar.

Why not lower?

LOW would mean either trivial mitigation in default configs or negligible impact; neither holds. Misconfigured clusters with --anonymous-auth=true or exposed read paths do exist, and a single frame causing repeatable kubelet OOM on every node in a cluster is operationally painful enough to clear MEDIUM. The bug is also reachable internally by any compromised pod that can dial the node IP.

05 · Compensating Control

What to do — in priority order.

  1. Confirm --anonymous-auth=false on every kubelet — This is the single control that turns a remote DoS into a closed door. Audit with kubectl get --raw /api/v1/nodes/<node>/proxy/configz or check the kubelet config file directly. MEDIUM verdict carries no noisgate mitigation SLA, but do this audit as part of the next monthly hardening cycle.
  2. Restrict network access to TCP/10250 — Cloud security groups / on-prem firewall rules should permit only the control-plane CIDR and approved monitoring agents to talk to kubelet. Block pod-to-node-IP egress to 10250 with a default-deny NetworkPolicy (Calico/Cilium GlobalNetworkPolicy). Same monthly hardening cadence — no formal mitigation SLA at MEDIUM.
  3. Inventory transitive use of moby/spdystream — Run go mod why github.com/moby/spdystream across every Go binary you build, and SBOM-scan vendor images. Anything pinning ≤ 0.5.0 lands on the patch queue. Complete within 30 days so you have a clean rollout target.
  4. Set per-process memory limits on kubelet — A systemd drop-in with MemoryMax= and Restart=always ensures one OOM does not cascade to host kernel panic and gives you fast self-healing while you wait on the binary update.
  5. Schedule the actual fix — Roll kubelet / CRI / Cilium / any spdystream consumer to a build that vendors 0.5.1+ within the noisgate remediation SLA (365 days for MEDIUM, but most cluster operators will pick this up automatically in the next minor bump).
What doesn't work
  • WAFs / API gateways — kubelet:10250 is not behind your edge WAF; this is a node-direct port.
  • Rate limiting at the LB — the bug fires on one frame, not a flood; rate limits do nothing.
  • EDR / runtime security tools — they cannot see SPDY frame internals, only the post-hoc kubelet crash.
  • Pinning kubectl client version — the bug is in the server parser; client version is irrelevant.
06 · Verification

Crowdsourced verification payload.

Run on each Linux node as root (or via your config-management tool) to check the spdystream version vendored into kubelet and the containerd/cri-o binary. Invocation: sudo ./check-cve-2026-35469.sh /usr/bin/kubelet. Requires read access to the binaries; no network calls.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check-cve-2026-35469.sh — detect vulnerable moby/spdystream (<= 0.5.0) inside a Go binary
# Usage: sudo ./check-cve-2026-35469.sh <path-to-go-binary> [more binaries...]
# Exits 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
set -u

FIXED="0.5.1"
status=0

ver_ge() {
  # returns 0 if $1 >= $2 (semver-ish)
  [ "$1" = "$2" ] && return 0
  printf '%s\n%s\n' "$2" "$1" | sort -V -C
}

check_one() {
  local bin="$1"
  if [ ! -r "$bin" ]; then
    echo "UNKNOWN  $bin  (cannot read)"
    return 2
  fi

  if ! command -v go >/dev/null 2>&1; then
    # Fall back to strings — less reliable but no toolchain needed
    local hit
    hit=$(strings "$bin" 2>/dev/null | grep -Eo 'github.com/moby/spdystream[[:space:]]*v[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
    if [ -z "$hit" ]; then
      echo "UNKNOWN  $bin  (no spdystream string; may be statically dropped or absent)"
      return 2
    fi
    local v=${hit##*v}
    if ver_ge "$v" "$FIXED"; then
      echo "PATCHED   $bin  spdystream $v"
      return 0
    fi
    echo "VULNERABLE $bin  spdystream $v  (fix: $FIXED)"
    return 1
  fi

  local mod
  mod=$(go version -m "$bin" 2>/dev/null | awk '$2=="dep" && $3 ~ /spdystream$/ {print $4; exit}')
  if [ -z "$mod" ]; then
    echo "UNKNOWN  $bin  (spdystream not in module table)"
    return 2
  fi
  local v=${mod#v}
  if ver_ge "$v" "$FIXED"; then
    echo "PATCHED   $bin  spdystream v$v"
    return 0
  fi
  echo "VULNERABLE $bin  spdystream v$v  (fix: v$FIXED)"
  return 1
}

if [ $# -eq 0 ]; then
  set -- /usr/bin/kubelet /usr/local/bin/kubelet \
         /usr/bin/containerd /usr/local/bin/containerd \
         /usr/bin/crio /usr/local/bin/crio \
         /usr/bin/cilium-agent /opt/cni/bin/cilium-cni
fi

for b in "$@"; do
  [ -e "$b" ] || continue
  check_one "$b"
  rc=$?
  [ $rc -gt $status ] && status=$rc
done

exit $status
07 · Bottom Line

If you remember one thing.

TL;DR
MEDIUM verdict — there is no noisgate mitigation SLA at this bucket, so go straight to the 365-day noisgate remediation SLA: roll kubelet, containerd, CRI-O, and any Go binary pulling moby/spdystream to a build that vendors 0.5.1+ within the next major patch cycle. Monday morning, kick off two parallel workstreams: (1) SBOM-scan every Go binary in your registry for moby/spdystream ≤ 0.5.0 and tag them for the next minor bump, (2) audit every kubelet for --anonymous-auth=false and a NetworkPolicy that blocks pod-to-node 10250 — that is the control which keeps this bug from ever being reachable. Skip the fire drill; this is hygiene, not a page.

Sources

  1. NVD — CVE-2026-35469
  2. SentinelOne vuln DB entry
  3. OffSeq Threat Radar — CWE-770 in moby/spdystream
  4. cvefeed.io — SpdyStream DOS on CRI
  5. Vulnerability-Lookup (CIRCL)
  6. SUSE CVE tracker
  7. Resolved Security advisory
  8. Go vulnerability reports
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.