VYPR
Moderate severityNVD Advisory· Published Mar 20, 2026· Updated Mar 23, 2026

CSI Driver for NFS path traversal via subDir may delete unintended directories on the NFS server

CVE-2026-3864

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].

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.

PackageAffected versionsPatched versions
github.com/kubernetes-csi/csi-driver-nfsGo
< 0.0.0-20260210055231-316af2d86b910.0.0-20260210055231-316af2d86b91

Affected products

2

Patches

1
316af2d86b91

Merge 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

News mentions

0

No linked articles in our index yet.