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

moby/spdystream ≤ 0.5.0 unbounded allocation in SPDY/3 frame parser (DoS)

ASSESSED — NOISGATE
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 · Compensating Control

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.
04 · 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.

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
05 · The Call

Final Verdict
= UNCHANGED to MEDIUM (5.3/10)

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.

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