Authorization Is Not Validation: The Kubernetes CSI Path Traversal Lesson
Two path-traversal bugs in Kubernetes storage drivers were patched this spring, and taken as isolated CVEs they're minor: medium severity, privileged access required, fixed. Taken as a pattern, they're a warning about how modern infrastructure fails. SentinelLabs' Shaul Ben Hai found both, and the write-up (with follow-on analysis from LinuxSecurity's Dave Wreski) draws out the lesson worth internalizing: a privileged component treating "the request was authorized" as "the request is safe" is a confused-deputy bug waiting to happen, and it recurs across every layer of the cloud-native stack.
The Two CVEs
Both are subDir path traversal in Kubernetes CSI drivers:
- CVE-2026-3864, the NFS driver (
nfs.csi.k8s.io). Fixed in csi-driver-nfs v4.13.1. Medium, CVSS 6.5. - CVE-2026-3865, the SMB driver (
smb.csi.k8s.io). Fixed in csi-driver-smb v1.20.1. Medium, CVSS 6.5.
The mechanism is the same in both. A user who can create a PersistentVolume referencing the driver crafts a volumeHandle (or subDir) containing traversal sequences like ../. When the driver runs cleanup during volume deletion, those sequences make it operate on directories outside the intended managed subdirectory, on the underlying NFS or SMB export. The result is deletion or modification of directories the driver was never meant to touch, and in some NFS controller configs, cross-tenant file access.
⚠️ Calibrate the severity honestly, because it matters for how you prioritize. The CVSS vector is PR:H, high privileges required. This is not an unauthenticated internet exploit. The attacker needs the ability to create PersistentVolumes referencing the CSI driver, which in a well-run cluster is a privileged operation restricted to administrators. So the immediate risk is real but bounded: it's a lateral-movement and blast-radius problem for someone who already has meaningful cluster access, or a multi-tenant cluster that hands PV-creation rights too freely. Patch it, but the more important work is the structural audit below, because the pattern is the actual threat, not this specific pair.
Where the Boundary Actually Broke
The interesting part is that every component did its job correctly. Walk the lifecycle of the malicious request:
- An authorized user or service account creates a PersistentVolume manifest with a crafted value inside
volumeHandle. - The Kubernetes API server receives it, authenticates the user, evaluates RBAC, and authorizes the creation. All standard, all correct. It has verified who is asking and whether they may create storage objects.
- The privileged CSI driver picks up the config, extracts
volumeHandle, passes it through Go path helpers likefilepath.Join(), and performs a mount, read, or delete on the underlying filesystem.
At no point did any layer independently verify that the resulting path stayed inside its intended boundary. Kubernetes treated volumeHandle as opaque and delegated its meaning to the driver. The driver assumed that a value the API server accepted was safe to act on. Automation on top assumed Kubernetes had already caught anything dangerous. Every component performed its assigned responsibility; the design assumption connecting them is what failed.
⚠️ The specific Go trap worth flagging, because it bites outside Kubernetes too: a normalized path is not a contained path. filepath.Join() cleans and combines path elements (collapsing ../ syntactically), but cleaning is not the same as guaranteeing the result stays within an intended root. Developers routinely assume filepath.Join() provides containment. It does not. For privileged code that resolves untrusted path input, you need a boundary-enforcing resolver like filepath-securejoin, not the standard-library join. If you write anything that turns user-supplied strings into filesystem operations under privilege, that distinction is the whole ballgame.
The Confused Deputy, Everywhere
The reason this is worth an article rather than a patch note: the same trust assumption has produced the same class of bug across unrelated projects. SentinelLabs lines up the history:
- Nimbuspwn relied on filesystem state staying trustworthy between validation and execution.
- runC (GHSA-c3xm-pvg7-gh7r) assumed normalized path syntax guaranteed a contained physical destination.
- Leaky Vessels lost track of context boundaries handling untrusted configs across namespaces.
Different projects, different researchers, different years, identical progression: a less-trusted layer supplies input, a privileged component inherits that trust, and no independent check confirms the operation stayed inside its boundary. That's the textbook confused deputy, a privileged component using its authority on behalf of a less-trusted requester without confirming the request target is appropriate.
The four security functions people conflate, and what each does not guarantee:
| Function | Answers | Does NOT guarantee |
|---|---|---|
| Authentication | Who submitted this? | That the identity or its environment isn't compromised |
| Authorization | May this identity do this? | That the request parameters are safe |
| Validation | Does the request meet safety and policy constraints? | That downstream execution introduces no new risk |
| Containment | Will the operation stay inside its approved boundary? | That the original requester was legitimate |
Most security reviews stop at row two. Authentication and authorization pass, the request is treated as legitimate, and rows three and four, validation and containment, are nobody's explicit job. This is the same "authorization is not validation" gap that showed up in the exposed-serverless metadata attack, where an authorized function with an over-scoped identity turned one bad parameter into a cloud takeover. Different layer, identical root cause.
Why It Keeps Recurring
Modern stacks are fragmented by design: orchestrator, CSI plugins, CNI plugins, GitOps agents, custom operators, container runtimes, kernel namespaces, LSM profiles. Responsibility for validating semantic data like a file path or a storage handle is spread so thin that no single component owns it end to end:
- Kubernetes assumes the storage driver validates storage-specific values.
- The driver assumes Kubernetes already filtered dangerous input.
- Automation assumes both layers did their part.
- Developers assume helper functions enforce containment.
When everyone assumes someone else validated the request, the boundary dissolves. Compounding it: infrastructure increasingly treats configuration as trusted data rather than untrusted input. Manifests, GitOps repos, Helm charts, Terraform state, and storage definitions flow through privileged systems with little semantic validation because they're perceived as infrastructure definitions, not user input. But a manifest a user can influence is user input, and this is exactly the risk in a GitOps pipeline where a signed commit is treated as implicitly safe all the way to a privileged controller, the trust-boundary concern I raised in building a secure GitOps pipeline.
Audit This, Don't Just Patch It
Upgrading the drivers (NFS to v4.13.1+, SMB to v1.20.1+) closes these two CVEs. It does nothing about the same assumption living elsewhere. The higher-value work is auditing where privileged components turn externally-supplied data into high-impact operations. Concretely:
Who can create raw storage assets across namespaces:
kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name | test("storage|admin|cluster-admin"))'
Whether PV creation is too broadly permitted (this is the exact privilege the CVEs require):
kubectl auth can-i create persistentvolumes --all-namespaces
Whether any admission-time validation actually runs before requests reach storage controllers:
kubectl get validatingadmissionpolicies
Which service accounts tied to GitOps and automation hold excess privilege:
kubectl get sa --all-namespaces -o json | jq '.items[].metadata | "\(.namespace)/\(.name)"'
Which pods mount host paths or run as privileged daemonsets:
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.volumes[*]}{.hostPath.path}{" "}{end}{"\n"}{end}' | grep -vE '\t$'
(That last one is the corrected form of the command the source truncated, it lists every pod with a hostPath volume so you can spot the ones with raw host filesystem access.)
The Preventive Control: Admission Policy
Detection can't save you here, and that's the strategic point. Because both the API request and the privileged process are authorized, a boundary failure initially looks like legitimate controller behavior. Your SIEM sees the consequence (a directory deleted where it shouldn't be), not the crossing. ⚠️ Detection is a compensating control; validation is a preventive one, and telemetry cannot reliably compensate for privileged software making an unsafe decision on authorized input.
The preventive fix is admission-time validation that inspects the driver-specific meaning of opaque values before they reach a privileged controller. ValidatingAdmissionPolicy (the in-tree CEL-based policy engine, GA since 1.30) or an external admission controller can enforce declarative constraints, for example, rejecting any volumeHandle or subDir containing ../ before the CSI driver ever sees it. This is the same admission-control layer that does the heavy lifting in the Kubernetes CIS benchmark work and pairs with the network-policy segmentation that caps blast radius when a boundary does fail. Schema validation alone isn't enough; a well-formed manifest with a malicious path passes schema checks cleanly.
Bottom Line
Patch the CSI drivers, but don't stop there, because the drivers were only the symptom. The disease is privileged components across the stack inheriting trust from an authorization decision that never promised the request was safe. Authorization answers "may they," not "is this safe," and in fragmented cloud-native infrastructure the gap between those two questions is where boundaries quietly dissolve. Audit who can create privileged storage and automation objects, enforce path and value validation at admission before requests hit privileged controllers, and remember the rule that generalizes far past Kubernetes: a normalized path is not a contained one, and an authorized request is not a validated one. The next boundary failure is already sitting in a layer nobody realized they owned.
References
- LinuxSecurity: Kubernetes Storage Vulnerabilities Demand Validation After Authorization (source, by Dave Wreski)
- SentinelLabs: Mount Here, Read There, Twin Path Traversal CVEs in Kubernetes Storage (source, by Shaul Ben Hai)
- Kubernetes Security Advisory: CVE-2026-3864 (NFS CSI Driver)
- Kubernetes Security Advisory: CVE-2026-3865 (SMB CSI Driver)
- filepath-securejoin: Secure Path Joining for Go
- Kubernetes: ValidatingAdmissionPolicy
- Saltzer and Schroeder: The Protection of Information in Computer Systems