A quotation mark in a YAML path field turns your ingress controller into a cluster-wide keyring dump
CVE-2026-3288 is a configuration injection in the nginx.ingress.kubernetes.io/rewrite-target handling path of ingress-nginx. The controller interpolates the Ingress spec.rules.http.paths.path value directly into a double-quoted nginx regex inside buildProxyPass() without escaping. A " character in the path closes the quoted string prematurely and the remainder is written into nginx.conf as raw directives — enough to load an arbitrary Lua block, exfiltrate the controller's ServiceAccount token, or shell out. Affected versions: ingress-nginx < 1.13.8, < 1.14.4, < 1.15.0. Fixed in 1.13.8 / 1.14.4 / 1.15.0, released 2026-03-04.
This is the *incomplete fix* to CVE-2026-24512. When maintainers introduced sanitizeQuotedRegex() in February 2026 to protect buildLocation(), they missed the identical sink in buildProxyPass(). The vendor's HIGH / 8.8 rating is accurate and arguably conservative — because the default ingress-nginx install ships a ClusterRole that can read Secrets *cluster-wide*, a successful injection turns any tenant with Ingress-write RBAC into a de facto cluster admin. In a shared/multi-tenant cluster the practical outcome is fleet-scale credential theft.
5 steps from start to impact.
Obtain Ingress create/update rights in any namespace
create/update on networking.k8s.io/ingresses in at least one namespace. In practice this is any app-team service account, developer kubeconfig, or CI runner in a shared cluster. In managed platforms (Rancher, OpenShift, GKE-shared) this is granted by default to project members.- Valid kubeconfig or SA token with Ingress-write RBAC in ≥1 namespace
- Target cluster runs vulnerable ingress-nginx < 1.13.8/1.14.4/1.15.0
- Fully locked-down clusters that restrict Ingress creation to a platform team via OPA/Kyverno reduce reachable population
- Some managed offerings (EKS with AWS Load Balancer Controller default) don't use ingress-nginx
Craft malicious Ingress with quote-terminated path
spec.rules.http.paths.path containing " followed by nginx directives, and the rewrite-target annotation set. Public PoC lives at github.com/SnailSploit/CVE-2026-3288 — the payload injects a content_by_lua_block that reads /var/run/secrets/kubernetes.io/serviceaccount/token.- Ingress admission webhook (ValidatingAdmissionPolicy) does not block quote characters in path fields
- No pre-admission Kyverno/OPA policy on rewrite-target
- Kubernetes 1.30+ ValidatingAdmissionPolicy can block the payload if operators wrote a rule
- Kyverno signed policies in production clusters often already restrict annotation values
Controller reloads nginx.conf with attacker directives
/etc/nginx/nginx.conf, and hot-reloads. Injected Lua now runs *inside* the controller pod on every matching request — and the controller pod runs as the ingress-nginx ServiceAccount.- Controller successfully validates and loads generated config (it does — the injection is syntactically valid nginx)
- None — this is the intended reconciliation path
Read cluster-wide Secrets via controller's ServiceAccount token
secrets get/list cluster-wide (required to read TLS material for arbitrary Ingress TLS blocks). Attacker Lua code calls the kube API with the mounted token and exfiltrates every Secret — including ServiceAccount tokens for cluster-admin bindings, cloud IAM credentials in workload Secrets, and CI/CD deploy keys.- Controller ServiceAccount has default cluster-wide Secret read (this is the shipped default in Helm chart and manifests)
- Rare hardened installs scope the controller ClusterRole to specific namespaces — this narrows blast radius but does not stop RCE inside the pod
secrets list requests from the ingress-nginx SA at unusual volume; cloud CSPM flags mass Secret enumerationPivot to cluster-admin or cloud tenant
- Any Secret in the cluster contains upstream credentials (near-universal)
- Workload Identity / IRSA / GKE Workload Identity Federation without static Secrets reduces cloud pivot value — but does NOT block cluster-admin token theft
" in Ingress path fields and restricting rewrite-target — Ship a Kyverno or ValidatingAdmissionPolicy (K8s 1.30+) that rejects any Ingress whose spec.rules[].http.paths[].path contains a " character or whose nginx.ingress.kubernetes.io/rewrite-target annotation contains suspicious characters. Deploy across all clusters within the noisgate HIGH mitigation window — ≤ 30 days — as a bridge until every cluster is on 1.13.8/1.14.4/1.15.0.kubectl auth can-i --list --as sweeps to enumerate every principal with create/update on ingresses. Remove access from CI service accounts, sandbox tenants, and low-trust developers. Do this before you deploy the patch — you'll want the pre/post delta for audit.secrets list API calls from the ingress-nginx SA above baseline. Alerts here catch the post-injection stage even if admission control is bypassed.ingress-nginx-4.13.8+) or manifest to the fixed version. Because ingress-nginx is a data-plane component, use a rolling replacement and validate custom snippets still template correctly. Complete within the noisgate HIGH remediation window — ≤ 180 days, but prioritize toward the front of that window.- WAF / ModSecurity in front of ingress-nginx — the injection happens at Ingress *admission* to the K8s API, not on data-plane traffic. The WAF never sees the payload.
- Network policies restricting pod-to-pod traffic — the controller's Secret access uses its mounted SA token against the API server; NetworkPolicy on the controller pod doesn't stop it (the API server is in kube-system).
- Making the LoadBalancer internal-only — same as above. The vulnerability is triggered via kube-apiserver, not the ingress data plane. An air-gapped cluster is still fully exposed to a tenant with kubectl access.
allow-snippet-annotations: false— this hardened CVE-2025-1974 (IngressNightmare) but does NOT block CVE-2026-3288, because the injection sink isrewrite-target, not a snippet annotation.
The supporting signals.
| In-the-wild status | No confirmed mass exploitation as of 2026-07-02; no KEV listing. Wiz and Sysdig both report active scanning against /healthz on internet-exposed ingress-nginx from Censys-indexed IPs post-disclosure. |
|---|---|
| Public PoC | Full walkthrough and payload at github.com/SnailSploit/CVE-2026-3288 — includes content_by_lua_block Secret-exfil variant. |
| EPSS | 0.06669 (~6.7% probability of exploitation in 30d) — elevated for a K8s add-on CVE requiring RBAC. |
| KEV status | Not listed on CISA KEV as of 2026-07-02. |
| CVSS vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — PR:L accurately reflects the required Ingress-write RBAC. Scope is U but blast radius exceeds a single component in practice. |
| Affected versions | ingress-nginx < 1.13.8, < 1.14.4, < 1.15.0. Same class of bug as CVE-2026-24512 (incomplete fix). |
| Fixed versions | 1.13.8, 1.14.4, 1.15.0 (released 2026-03-04). Helm chart bumps: ingress-nginx-4.13.8, 4.14.4, 4.15.0. |
| Exposure data | Shodan/Censys count ~340k internet-reachable ingress-nginx instances (server header fingerprint); Sysdig telemetry shows ingress-nginx present in ~44% of surveyed K8s clusters. |
| Disclosure | Reported by Nir Ohfeld & Sagi Tzadik (Wiz Research); coordinated disclosure via Kubernetes SRC. Public advisory 2026-03-09 on oss-security. |
| Reporter | Wiz Research, credited jointly with the Kubernetes Security Response Committee (kubernetes-security@). |
Sources.
Why this verdict
- Role multiplier — ingress-nginx is infrastructure, not app tier: the default Helm install grants the controller SA
secretsread cluster-wide. RCE in that pod = every Secret in the cluster, which means every ServiceAccount token, every TLS key, every cloud workload credential. Chain reliably ends in cluster-admin. - Friction floor — PR:L is a low bar in multi-tenant clusters: most enterprise K8s clusters grant Ingress-write to project/namespace admins by default. In a shared cluster this is effectively tenant-to-cluster-admin escalation with a one-line YAML payload.
- Installed base is enormous: ingress-nginx is the most-deployed K8s ingress controller (~44% of clusters per Sysdig, ~340k internet-reachable instances per Censys). Even with
PR:Lgating, the reachable population is large. - No downgrade for exposure: even air-gapped/private clusters are in scope — the attacker vector is a K8s API call, not an HTTP request to the ingress data plane. Firewalling the LB does not help.
- Not CRITICAL: no unauthenticated path, no KEV, no confirmed campaign, EPSS still under 10%.
PR:Lis meaningful enough to keep this out of the CRITICAL bucket for now.
Why not higher?
No unauthenticated remote path — the attacker must already have an authenticated Kubernetes identity with Ingress-write RBAC. No KEV listing and no confirmed mass-exploitation campaign as of 2026-07-02. EPSS remains under 10%. If a campaign emerges (Wiz/Sysdig telemetry begins showing post-exploitation Secret enumeration in the wild), this becomes CRITICAL immediately.
Why not lower?
MEDIUM would ignore the deployment-role floor: ingress-nginx *is* a canonical high-value component, cluster-wide Secret read is the shipped default, and a public PoC exists. Downgrading to MEDIUM would require evidence that the vast majority of installs restrict the controller ClusterRole and enforce admission policies on Ingress annotations — neither is the industry norm.
Crowdsourced verification payload.
Run this on an auditor workstation with a kubeconfig that has read access to the target cluster (kubectl get pods -A should work). Invoke as ./check-cve-2026-3288.sh [context-name]. Reads the deployed ingress-nginx image tag and compares against fixed versions. Requires kubectl and jq.
#!/usr/bin/env bash
# noisgate verifier: CVE-2026-3288 (ingress-nginx rewrite-target injection)
# Exit: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
set -u
CTX="${1:-}"
KUBECTL="kubectl"
[ -n "$CTX" ] && KUBECTL="kubectl --context=$CTX"
command -v jq >/dev/null || { echo "UNKNOWN: jq missing"; exit 2; }
command -v kubectl >/dev/null || { echo "UNKNOWN: kubectl missing"; exit 2; }
# Find any Deployment/DaemonSet running an ingress-nginx controller image
PODS=$($KUBECTL get pods -A -o json 2>/dev/null | \
jq -r '.items[] | select(.spec.containers[]?.image | test("ingress-nginx/controller")) | \
[.metadata.namespace, .metadata.name, (.spec.containers[] | select(.image|test("ingress-nginx/controller")) | .image)] | @tsv')
if [ -z "$PODS" ]; then
echo "UNKNOWN: no ingress-nginx controller pods found in this cluster"
exit 2
fi
vuln=0
unknown=0
while IFS=$'\t' read -r ns name image; do
tag=$(echo "$image" | sed -E 's/.*:v?([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if ! echo "$tag" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "UNKNOWN: $ns/$name image=$image (cannot parse tag)"
unknown=1
continue
fi
IFS=. read -r maj min pat <<< "$tag"
# Fixed: 1.13.8, 1.14.4, 1.15.0 (and any 1.15+ / 1.x >1.15)
fixed=1
if [ "$maj" -eq 1 ]; then
if [ "$min" -lt 13 ]; then fixed=0
elif [ "$min" -eq 13 ] && [ "$pat" -lt 8 ]; then fixed=0
elif [ "$min" -eq 14 ] && [ "$pat" -lt 4 ]; then fixed=0
fi
elif [ "$maj" -lt 1 ]; then
fixed=0
fi
if [ "$fixed" -eq 0 ]; then
echo "VULNERABLE: $ns/$name running $tag"
vuln=1
else
echo "PATCHED: $ns/$name running $tag"
fi
done <<< "$PODS"
if [ "$vuln" -eq 1 ]; then exit 1; fi
if [ "$unknown" -eq 1 ]; then exit 2; fi
exit 0