Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions pkg/blob/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,50 @@ func (d *Driver) useDataPlaneAPI(ctx context.Context, volumeID, accountName stri
return false
}

// blockedEphemeralMountOptions is the set of blobfuse2 options that users must not be able to
// override via volumeAttributes.mountOptions on inline ephemeral volumes.
//
// --tmp-path: primary exploit vector (file-cache mode). blobfuse2 writes blob content
// (attacker-controlled) into this directory as root. Setting it to e.g.
// /etc/kubernetes/manifests allows the attacker to land an arbitrary static Pod manifest,
// achieving host-level RCE.
//
// --block-cache-path: same exploit vector as --tmp-path but for block-cache mode. blobfuse2
// persists downloaded blocks (attacker-controlled blob content) to this directory as root.
// An ephemeral volume with --block-cache-path=/etc/kubernetes/manifests (optionally combined
// with --block-cache=true) reproduces the same static-Pod-injection / host RCE primitive.
//
// --config-file: two-step bypass for the above blocks. An attacker can upload a blobfuse2
// config YAML as a blob (step 1, normal mount caches it to /mnt/<volumeID>/evil.yaml), then
// point a second ephemeral mount at that cached file via --config-file, which sets
// file_cache.path or block_cache.path inside the config, reinstating the RCE path.
var blockedEphemeralMountOptions = []string{
"--tmp-path",
Comment thread
kapilupadhayay marked this conversation as resolved.
Comment thread
kapilupadhayay marked this conversation as resolved.
"--block-cache-path",
"--config-file",
}

// sanitizeMountOptions removes options from the provided list that are in the
// blockedEphemeralMountOptions denylist. It is called when processing user-supplied mount options
// for inline ephemeral CSI volumes before the driver appends its own safe defaults.
func sanitizeMountOptions(mountOptions []string) []string {
filtered := make([]string, 0, len(mountOptions))
for _, opt := range mountOptions {
blocked := false
for _, blockedPrefix := range blockedEphemeralMountOptions {
if strings.HasPrefix(strings.TrimSpace(opt), blockedPrefix) {
klog.Warningf("mount option %q is not allowed for ephemeral volumes and will be ignored", opt)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a --temp-path*" mount that is ok? Any mount option starting with temp-path` will be blocked in this code path.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the short form of these mount options?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the check exact. None of the options have shorter forms.

blocked = true
break
}
}
if !blocked {
filtered = append(filtered, opt)
}
}
return filtered
}

// appendDefaultMountOptions return mount options combined with mountOptions and defaultMountOptions
func appendDefaultMountOptions(mountOptions []string, tmpPath, containerName string) []string {
var defaultMountOptions = map[string]string{
Expand Down
58 changes: 58 additions & 0 deletions pkg/blob/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,64 @@ func TestUseDataPlaneAPI(t *testing.T) {
}
}

func TestSanitizeMountOptions(t *testing.T) {
tests := []struct {
name string
options []string
expected []string
}{
{
name: "no blocked options passes through unchanged",
options: []string{"--use-https=true", "--cancel-list-on-mount-seconds=10"},
expected: []string{"--use-https=true", "--cancel-list-on-mount-seconds=10"},
},
{
name: "--tmp-path is stripped",
options: []string{"--use-https=true", "--tmp-path=/etc/kubernetes/manifests"},
expected: []string{"--use-https=true"},
},
{
name: "--block-cache-path is stripped",
options: []string{"--block-cache-path=/etc/kubernetes/manifests", "--use-https=true"},
expected: []string{"--use-https=true"},
},
{
name: "--config-file is stripped",
options: []string{"--config-file=/attacker/config", "--use-https=true"},
expected: []string{"--use-https=true"},
},
{
name: "--log-file is NOT blocked and passes through",
options: []string{"--log-file=/var/log/blobfuse.log", "--use-https=true"},
expected: []string{"--log-file=/var/log/blobfuse.log", "--use-https=true"},
},
{
name: "all three blocked options are stripped together",
options: []string{"--tmp-path=/etc/kubernetes/manifests", "--block-cache-path=/etc/cron.d", "--config-file=/evil", "--use-https=true"},
expected: []string{"--use-https=true"},
},
{
name: "empty input returns empty output",
options: []string{},
expected: []string{},
},
{
name: "option with leading whitespace is still blocked",
options: []string{" --tmp-path=/etc/kubernetes/manifests"},
expected: []string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := sanitizeMountOptions(test.options)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("sanitizeMountOptions(%v) = %v, want %v", test.options, result, test.expected)
}
})
}
}

func TestAppendDefaultMountOptions(t *testing.T) {
tests := []struct {
options []string
Expand Down
5 changes: 4 additions & 1 deletion pkg/blob/nodeserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,10 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe
// Get mountOptions that the volume will be formatted and mounted with
mountOptions := mountFlags
if ephemeralVol {
mountOptions = util.JoinMountOptions(mountOptions, strings.Split(ephemeralVolMountOptions, ","))
// Sanitize user-supplied mount options before use: strip options that are
// security-sensitive (e.g. --tmp-path) and must be driver-controlled.
sanitized := sanitizeMountOptions(strings.Split(ephemeralVolMountOptions, ","))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it silently sanitize and mount or should it throw an error saying that it can't mount with provided options?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer this failing with InvaidArgument error as the user isn't getting the requested behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its better to fail with InvalidArg.

mountOptions = util.JoinMountOptions(mountOptions, sanitized)
}
if isHnsEnabled {
mountOptions = util.JoinMountOptions(mountOptions, []string{"--use-adls=true"})
Expand Down
Loading