Skip to main content

The Kubernetes CSI Driver Bug That Turns a Tenant's subDir Into a Delete Weapon

9 min readDora NodaDora Noda
Share

Two Kubernetes CSI drivers — the ones a self-hosted platform reaches for the moment a tenant asks for a shared, network-backed volume instead of plain node storage — shipped with a path traversal bug in the exact field that names which directory gets deleted. CVE-2026-3864 (CSI Driver for NFS, disclosed March 17, 2026) and CVE-2026-3865 (CSI Driver for SMB, disclosed April 10, 2026) both let a crafted subDir value walk out of the directory a volume is supposed to own and delete or modify something else on the shared export. "The bug was in the storage driver, not our app" is true and irrelevant — it's still your fleet's export getting corrupted, and your tenants' data sitting on it.

What actually broke

Both CVEs are the same bug in two sibling drivers: insufficient validation of the subDir value embedded in a PersistentVolume's volumeHandle.

CVECVSSMechanismAffectedFixed
CVE-2026-3864 — CSI Driver for NFS6.5 Medium (AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H)subDir in the volume identifier isn't checked for traversal sequences like ../All versions prior to v4.13.1v4.13.1
CVE-2026-3865 — CSI Driver for SMB6.5 Medium (same vector)Same validation gap, same field, SMB export instead of NFSAll versions prior to v1.20.1v1.20.1

Here's the mechanism in plain terms. A PersistentVolume backed by one of these drivers carries a volumeHandle that encodes, among other things, the subDir the driver should treat as that volume's root — the one subdirectory of the NFS or SMB export it's allowed to touch. When Kubernetes calls DeleteVolume during cleanup, the driver takes that subDir value and removes it off the export root. Neither driver rejected a subDir containing ../ segments before this fix. Feed it something like legit-volume/../../../shared-export/someone-elses-data and the driver's own cleanup routine walks straight out of the sandboxed subdirectory and deletes or overwrites whatever sits at the other end — using the driver's own credentials, no exploit beyond writing a string.

Read the CVSS vector closely and it tells you exactly what kind of damage this is: C:N/I:H/A:H — no confidentiality impact, but high integrity and high availability impact. This isn't a file-read bug; an attacker can't use it to exfiltrate another tenant's data. It's a destructive-write bug — deletion or corruption of directories the attacker was never supposed to touch, anywhere on the shared export the driver's service account can reach. For a platform storing tenant volumes, uploaded assets, or shared build caches on that export, "can't read it, can delete it" is still a full outage for whoever owned that directory, with no warning before it happens.

Both were reported by Shaul Ben Hai (SentinelOne) and fixed by the driver maintainers in coordination with the Kubernetes Security Response Committee. Neither is listed in CISA's Known Exploited Vulnerabilities catalog as of this writing — this is a disclosed, patched bug, not an active campaign. The threat model is the same as any well-documented CVE: not a zero-day in the wild, but a recipe anyone can reproduce from the advisory.

Who can actually pull the trigger — and why that's not reassuring on a PaaS

Look at the CVSS vector again: PR:H. Privileges required: high. The disclosed attack path is static provisioning — someone with the RBAC to directly author a PersistentVolume object crafts the malicious volumeHandle by hand. PersistentVolume is cluster-scoped, not namespaced, so an ordinary tenant on a shared platform almost never has create on it. That single fact makes the bug look, at first read, like a non-issue for anyone running multi-tenant Kubernetes with sane RBAC: the people who could exploit it are the people you already trust.

That reading holds for a hand-administered cluster. It doesn't hold for a self-hosted PaaS. On a platform where tenants push a git repo and get a running service, nobody's human hand is writing PersistentVolume YAML — the platform's own control plane is, on the tenant's behalf, every time someone provisions a volume through a one-click feature or a bex.yml-declared mount. That control plane runs with the elevated RBAC a human admin would otherwise need. If the code path that builds a volumeHandle or a templated subDir for a tenant's volume folds in anything the tenant supplied — an app slug, a database name, a volume label — without treating it as untrusted, the platform's own automation becomes the confused deputy: it holds the PR:H privilege the CVE assumes only an admin would have, and a tenant's ordinary, low-privilege input is what walks through it.

To be clear, that specific "controller trusts unsanitized tenant input into a subDir template" path is my own architectural read of why this matters for a self-hosted PaaS — it's not a mechanism the Kubernetes advisory itself describes or tested. The advisory's disclosed vector is a directly hand-authored volumeHandle. The extension to templated dynamic provisioning is the same bug class (an unsanitized path segment reaching a delete operation), reasoned by analogy, and it's exactly the gap the audit below is built to close regardless of which path actually carries tenant input on your fleet.

How to tell if you're exposed

Three checks, all read-only:

  1. Version check. helm list -A | grep -E 'csi-driver-(nfs|smb)' or check the container image tag on the driver's controller pod directly. Anything below v4.13.1 (NFS) or v1.20.1 (SMB) is vulnerable.
  2. Audit existing PVs. kubectl get pv -o json | jq '.items[] | select(.spec.csi.driver | test("nfs|smb")) | .spec.csi.volumeHandle' and grep the output for ../ or URL-encoded traversal sequences. Any hit is a PV that should not exist and needs manual review before you touch it.
  3. Check controller logs for the tell. A completed exploit leaves a distinctive line in the CSI controller's DeleteVolume logs — something in the shape of removing subPath: /tmp/mount-<uuid>/legitimate/../../../exports/subdir. A .. inside a path Kubernetes logged as "removing" is not a formatting artifact; treat it as an incident.

None of these three checks require touching production traffic or the export itself — they're all read-only queries against the Kubernetes API and log storage you already have, which is why there's no excuse for skipping the audit even on a fleet where you're fairly confident RBAC already blocks the disclosed vector. The point of the audit isn't to catch a live attacker; on a Medium-severity, non-KEV-listed bug, that's unlikely. It's to produce a documented "we checked" you can point to the next time a tenant or a compliance questionnaire asks how storage-layer CVEs get triaged on your fleet.

The actual fix: stop letting any code path author a raw PV

Patching the driver version closes the disclosed bug. It doesn't close the architectural gap the TODO for this piece specifically asks about: what should a platform that provisions volumes on tenants' behalf actually do differently?

The answer Kubernetes' own storage model already points to is dynamic provisioning through an operator-owned StorageClass, not static, hand-authored PVs. In dynamic provisioning, tenants (or your control plane on their behalf) create a PersistentVolumeClaim — a namespaced, low-privilege object — and the CSI external-provisioner sidecar is the only thing that ever calls CreateVolume and constructs the resulting PV. That collapses the exposed surface from "anything with PV-create rights, including our own automation" down to one component whose input handling you can actually audit in one place, and it matches how csi-driver-nfs's own StorageClass parameters are designed to work — subDir there can be a fixed operator-set value, not a per-request string threaded through from user input.

If your StorageClass does use templating (csi-driver-nfs supports variables like ${pvc.metadata.name} in subDir), audit that path specifically: whatever populates the PVC name or any other templated field needs the same untrusted-input treatment as request-body input to an API endpoint — reject .., reject leading /, allowlist the character set. That's true even though it's not the literal CVE-disclosed vector, because the failure mode — an unsanitized path segment reaching a driver's delete logic — is identical.

Patch and audit checklist

  1. Upgrade the drivers. csi-driver-nfs ≥ v4.13.1, csi-driver-smb ≥ v1.20.1. Check both independently — they're separate Helm charts and separate release cadences, and a fleet that patched one often forgets the other exists.
  2. Audit every existing PV's volumeHandle for traversal sequences before you assume a clean upgrade is sufficient — the patch stops new exploitation, it doesn't retroactively clean up a PV someone already planted.
  3. Lock RBAC on persistentvolumes create/update/patch down to the platform's own provisioning controller's service account. No tenant-facing code path, and no human operator doing a one-off fix, should have standing access to author a PV directly.
  4. Route every tenant volume through a StorageClass, never a hand-built PV, and treat any templated field in that StorageClass (PVC name, namespace, labels) as untrusted input at the point it's constructed.
  5. Prefer local or block storage for anything where tenant isolation matters more than sharing. A Hetzner-CSI-backed block volume scoped to one tenant's own node has no shared export to traverse into in the first place — the whole bug class only exists because NFS/SMB volumes share a mount namespace across tenants by design.

The blast radius is always yours, not the driver maintainer's

The CSI maintainers did the right thing here — coordinated disclosure, a credited external researcher, a same-week patch for both drivers. None of that changes who's paged when a shared export loses a directory it shouldn't have lost: whoever operates the fleet. For a platform whose entire pitch is "we provision infrastructure on your behalf," an upstream CVE in a component you didn't write is still a finding in your own security review, because the privilege the CVE assumes belongs to a careless admin belongs, on your platform, to your own control plane.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, orchestrated by Cluster API on hardware you actually control. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex