CSI Driver for NFS path traversal via subDir may delete unintended directories on the NFS server
Description
A vulnerability was discovered in the Kubernetes CSI Driver for NFS where the subDir parameter in volume identifiers was insufficiently validated. Attackers with the ability to create PersistentVolumes referencing the NFS CSI driver could craft volume identifiers containing path traversal sequences (../). During volume deletion or cleanup operations, the driver could operate on unintended directories outside the intended managed path within the NFS export. This may lead to deletion or modification of directories on the NFS server.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-3864 is a path traversal vulnerability in the Kubernetes CSI Driver for NFS that allows attackers with PersistentVolume creation privileges to delete or modify directories outside the intended NFS export path.
Vulnerability
Overview
CVE-2026-3864 is a path traversal vulnerability in the Kubernetes CSI Driver for NFS (nfs.csi.k8s.io). The root cause is insufficient validation of the subDir parameter in volume identifiers. Attackers who can create PersistentVolumes referencing the NFS CSI driver can craft a volumeHandle containing traversal sequences such as ../. During volume deletion or cleanup operations, the driver may operate on unintended directories outside the intended managed path within the NFS export [1][2][4].
Exploitation
To exploit this vulnerability, an attacker must have the ability to create PersistentVolumes that reference the NFS CSI driver. This is typically a high-privilege operation in Kubernetes clusters, but may be granted to some users or service accounts. The attacker crafts a volumeHandle with path traversal sequences (e.g., ../) in the subDir field. When the driver performs cleanup operations during volume deletion, such as during volume deletion, it follows the traversal path and can delete or modify directories on the NFS server that are outside the intended managed subdirectory [2][4].
Impact
Successful exploitation allows an attacker to delete or modify directories on the NFS server that are outside the intended managed path. This could lead to data loss or corruption, or disruption of services relying on the NFS export. The CVSS 3.1 base score is 6.5 (Medium), with a vector of AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H, indicating high impact on integrity and availability [4].
Mitigation
The vulnerability is fixed in CSI Driver for NFS version v4.13.1 and later [4]. Mitigations include upgrading to the patched version, restricting PersistentVolume creation privileges to trusted administrators, and reviewing NFS exports to ensure only intended directories are writable by the driver. As a best practice, untrusted users should not be granted permission to create arbitrary PersistentVolumes referencing external storage drivers [2][4].
- Merge pull request #1040 from andyzhangx/validate-path · kubernetes-csi/csi-driver-nfs@316af2d
- security - [kubernetes] CVE-2026-3864: CSI Driver for NFS path traversal via subDir may delete unintended directories on the NFS server
- [Security Advisory] CVE-2026-3864: CSI Driver for NFS path traversal via subDir may delete unintended directories on the NFS server
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kubernetes-csi/csi-driver-nfsGo | < 0.0.0-20260210055231-316af2d86b91 | 0.0.0-20260210055231-316af2d86b91 |
Affected products
2- Kubernetes/CSI Driver for NFSv5Range: 0
Patches
1316af2d86b91Merge pull request #1040 from andyzhangx/validate-path
5 files changed · +308 −0
pkg/nfs/controllerserver.go+7 −0 modified@@ -818,6 +818,13 @@ func getNfsVolFromID(id string) (*nfsVolume, error) { } } + if err := validatePath(subDir); err != nil { + return nil, fmt.Errorf("invalid subDir %q: %v", subDir, err) + } + if err := validatePath(baseDir); err != nil { + return nil, fmt.Errorf("invalid baseDir %q: %v", baseDir, err) + } + return &nfsVolume{ id: id, server: server,
pkg/nfs/controllerserver_test.go+215 −0 modified@@ -1307,3 +1307,218 @@ func TestArchiveNameWithCompression(t *testing.T) { t.Errorf("expected 'test-volume.tar', got %q", nameWithoutCompression) } } + +func TestGetNfsVolFromID(t *testing.T) { + cases := []struct { + name string + volumeID string + expected *nfsVolume + expectErr bool + }{ + { + name: "empty volume ID", + volumeID: "", + expected: nil, + expectErr: true, + }, + { + name: "only server", + volumeID: "test-server", + expected: nil, + expectErr: true, + }, + { + name: "missing subDir with slash separator", + volumeID: "test-server/test-base-dir", + expected: nil, + expectErr: true, + }, + { + name: "missing subDir with hash separator", + volumeID: "test-server#test-base-dir", + expected: nil, + expectErr: true, + }, + { + name: "invalid subDir with directory traversal using slash separator", + volumeID: "test-server/test-base-dir/../passwd", + expected: nil, + expectErr: true, + }, + { + name: "valid subDir with directory using slash separator", + volumeID: "test-server/test-base-dir/foo..bar/passwd", + expected: &nfsVolume{ + id: "test-server/test-base-dir/foo..bar/passwd", + server: "test-server", + baseDir: "test-base-dir/foo..bar", + subDir: "passwd", + }, + expectErr: false, + }, + { + name: "valid old format with slash separator", + volumeID: "test-server/test-base-dir/volume-name", + expected: &nfsVolume{ + id: "test-server/test-base-dir/volume-name", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + }, + expectErr: false, + }, + { + name: "valid old format with nested baseDir", + volumeID: "test-server/test/base/dir/volume-name", + expected: &nfsVolume{ + id: "test-server/test/base/dir/volume-name", + server: "test-server", + baseDir: "test/base/dir", + subDir: "volume-name", + }, + expectErr: false, + }, + { + name: "valid new format with hash separator - minimal", + volumeID: "test-server#test-base-dir#volume-name", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + }, + expectErr: false, + }, + { + name: "valid new format with uuid", + volumeID: "test-server#test-base-dir#volume-name#uuid-value", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name#uuid-value", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "uuid-value", + }, + expectErr: false, + }, + { + name: "valid new format with uuid and onDelete retain", + volumeID: "test-server#test-base-dir#volume-name#uuid-value#retain", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name#uuid-value#retain", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "uuid-value", + onDelete: "retain", + }, + expectErr: false, + }, + { + name: "valid new format with uuid and onDelete delete", + volumeID: "test-server#test-base-dir#volume-name#uuid-value#delete", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name#uuid-value#delete", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "uuid-value", + onDelete: "delete", + }, + expectErr: false, + }, + { + name: "valid new format with uuid and onDelete archive", + volumeID: "test-server#test-base-dir#volume-name#uuid-value#archive", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name#uuid-value#archive", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "uuid-value", + onDelete: "archive", + }, + expectErr: false, + }, + { + name: "valid new format with empty uuid", + volumeID: "test-server#test-base-dir#volume-name##archive", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name##archive", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "", + onDelete: "archive", + }, + expectErr: false, + }, + { + name: "valid new format with nested baseDir", + volumeID: "test-server#test/base/dir#volume-name#uuid", + expected: &nfsVolume{ + id: "test-server#test/base/dir#volume-name#uuid", + server: "test-server", + baseDir: "test/base/dir", + subDir: "volume-name", + uuid: "uuid", + }, + expectErr: false, + }, + { + name: "valid new format with FQDN server", + volumeID: "nfs-server.default.svc.cluster.local#share#pvc-12345##", + expected: &nfsVolume{ + id: "nfs-server.default.svc.cluster.local#share#pvc-12345##", + server: "nfs-server.default.svc.cluster.local", + baseDir: "share", + subDir: "pvc-12345", + uuid: "", + onDelete: "", + }, + expectErr: false, + }, + { + name: "valid new format with IP address server", + volumeID: "192.168.1.100#exports#volume-name#uuid#retain", + expected: &nfsVolume{ + id: "192.168.1.100#exports#volume-name#uuid#retain", + server: "192.168.1.100", + baseDir: "exports", + subDir: "volume-name", + uuid: "uuid", + onDelete: "retain", + }, + expectErr: false, + }, + { + name: "valid format with extra segments", + volumeID: "test-server#test-base-dir#volume-name#uuid#retain#extra", + expected: &nfsVolume{ + id: "test-server#test-base-dir#volume-name#uuid#retain#extra", + server: "test-server", + baseDir: "test-base-dir", + subDir: "volume-name", + uuid: "uuid", + onDelete: "retain", + }, + expectErr: false, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + result, err := getNfsVolFromID(test.volumeID) + + if test.expectErr && err == nil { + t.Errorf("expected error but got nil") + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("got %+v, expected %+v", result, test.expected) + } + }) + } +}
pkg/nfs/nodeserver.go+4 −0 modified@@ -115,6 +115,10 @@ func (ns *NodeServer) NodePublishVolume(_ context.Context, req *csi.NodePublishV source = fmt.Sprintf("%s/%s", source, subDir) } + if err := validatePath(source); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid volume source %q: %v", source, err) + } + notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath) if err != nil { if os.IsNotExist(err) {
pkg/nfs/utils.go+9 −0 modified@@ -307,3 +307,12 @@ func getVolumeCapabilityFromSecret(volumeID string, secret map[string]string) *c } return nil } + +func validatePath(path string) error { + for _, segment := range strings.Split(path, "/") { + if segment == ".." { + return fmt.Errorf("path contains directory traversal sequence") + } + } + return nil +}
pkg/nfs/utils_test.go+73 −0 modified@@ -554,3 +554,76 @@ func TestGetVolumeCapabilityFromSecret(t *testing.T) { }) } } + +func TestValidatePath(t *testing.T) { + tests := []struct { + desc string + path string + expected error + }{ + { + desc: "valid path", + path: "/home/user/data", + expected: nil, + }, + { + desc: "valid relative path", + path: "user/data/file.txt", + expected: nil, + }, + { + desc: "empty path", + path: "", + expected: nil, + }, + { + desc: "root path", + path: "/", + expected: nil, + }, + { + desc: "path with single dot", + path: "/home/./user", + expected: nil, + }, + { + desc: "path with directory traversal at start", + path: "../etc/passwd", + expected: fmt.Errorf("path contains directory traversal sequence"), + }, + { + desc: "path with directory traversal in middle", + path: "/home/../etc/passwd", + expected: fmt.Errorf("path contains directory traversal sequence"), + }, + { + desc: "path with directory traversal at end", + path: "/home/user/..", + expected: fmt.Errorf("path contains directory traversal sequence"), + }, + { + desc: "path with multiple directory traversals", + path: "/home/../../etc/passwd", + expected: fmt.Errorf("path contains directory traversal sequence"), + }, + { + desc: "path with only directory traversal", + path: "..", + expected: fmt.Errorf("path contains directory traversal sequence"), + }, + { + desc: "path with triple dots (valid)", + path: "/home/.../data", + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + result := validatePath(test.path) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("test[%s]: unexpected output: %v, expected result: %v", test.desc, result, test.expected) + } + }) + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-2mjq-54qg-7w6jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-3864ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/03/17/1ghsaWEB
- github.com/kubernetes-csi/csi-driver-nfs/commit/316af2d86b913a595542dc17e8599cf81afd938fghsaWEB
- github.com/kubernetes/kubernetes/issues/137797ghsaissue-trackingWEB
- groups.google.com/g/kubernetes-security-announce/c/i4ZKN9VLcUEghsamailing-listWEB
News mentions
0No linked articles in our index yet.