VYPR
Moderate severityNVD Advisory· Published Mar 15, 2024· Updated Aug 2, 2024

OS Command Injection for Fluid Users with JuicefsRuntime

CVE-2023-51699

Description

Fluid is an open source Kubernetes-native Distributed Dataset Orchestrator and Accelerator for data-intensive applications. An OS command injection vulnerability within the Fluid project's JuicefsRuntime can potentially allow an authenticated user, who has the authority to create or update the K8s CRD Dataset/JuicefsRuntime, to execute arbitrary OS commands within the juicefs related containers. This could lead to unauthorized access, modification or deletion of data. Users who're using versions < 0.9.3 with JuicefsRuntime should upgrade to v0.9.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Fluid JuicefsRuntime OS command injection allows authenticated users to execute arbitrary commands via unsanitized input.

CVE-2023-51699 is an OS command injection vulnerability in the JuicefsRuntime component of Fluid, a Kubernetes-native dataset orchestrator [1]. The root cause is insufficient sanitization of user-controlled strings when constructing command arguments for the JuiceFS FUSE mount, as shown in the fixes that introduced a security.EscapeBashStr function [2][3].

An authenticated user with permissions to create or update the Dataset or JuicefsRuntime custom resources can inject arbitrary shell commands via the mount name, mount path, or options fields. No additional network access is required beyond Kubernetes API access [1].

Successful exploitation allows the attacker to execute arbitrary OS commands within the juicefs containers, potentially leading to data access, modification, or deletion [1].

The vulnerability is patched in Fluid v0.9.3. Users should upgrade immediately. No known workarounds are documented [1][2][3].

AI Insight generated on May 20, 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/fluid-cloudnative/fluidGo
< 0.9.30.9.3

Affected products

3

Patches

2
02b7cd8b79a2

Fix JuicefsRuntime: escape customized string before constructing commands (#3761)

8 files changed · +173 38
  • charts/juicefs/Chart.yaml+1 1 modified
    @@ -1,7 +1,7 @@
     name: juicefs
     apiVersion: v2
     description: FileSystem aimed for data analytics and machine learning in any cloud.
    -version: 0.2.15
    +version: 0.2.16
     appVersion: v1.0.0
     home: https://juicefs.com/
     maintainers:
    
  • pkg/ddc/juicefs/operations/base.go+14 9 modified
    @@ -28,6 +28,7 @@ import (
     
     	"github.com/fluid-cloudnative/fluid/pkg/utils/cmdguard"
     	"github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient"
    +	"github.com/fluid-cloudnative/fluid/pkg/utils/security"
     )
     
     type JuiceFileUtils struct {
    @@ -113,12 +114,11 @@ func (j JuiceFileUtils) Count(juiceSubPath string) (total int64, err error) {
     func (j JuiceFileUtils) GetFileCount(juiceSubPath string) (fileCount int64, err error) {
     	var (
     		//strs    = "du -ah juiceSubPath |grep ^- |wc -l "
    -		strs    = fmt.Sprintf("ls -lR %s |grep ^- |wc -l ", juiceSubPath)
    +		strs    = fmt.Sprintf("ls -lR %s |grep ^- |wc -l ", security.EscapeBashStr(juiceSubPath))
     		command = []string{"bash", "-c", strs}
     		stdout  string
     		stderr  string
     	)
    -
     	stdout, stderr, err = j.exec(command)
     	if err != nil {
     		err = fmt.Errorf("execute command %v with expectedErr: %v stdout %s and stderr %s", command, err, stdout, stderr)
    @@ -249,11 +249,10 @@ func (j JuiceFileUtils) GetMetric(juicefsPath string) (metrics string, err error
     }
     
     // GetUsedSpace Get used space in byte
    -// use "df --block-size=1 |grep <juicefsPath>'"
    +// equal to `df --block-size=1 | grep juicefsPath`
     func (j JuiceFileUtils) GetUsedSpace(juicefsPath string) (usedSpace int64, err error) {
     	var (
    -		strs    = fmt.Sprintf(`df --block-size=1 |grep %s`, juicefsPath)
    -		command = []string{"bash", "-c", strs}
    +		command = []string{"df", "--block-size=1"}
     		stdout  string
     		stderr  string
     	)
    @@ -264,9 +263,15 @@ func (j JuiceFileUtils) GetUsedSpace(juicefsPath string) (usedSpace int64, err e
     		return
     	}
     
    +	var str string
    +	lines := strings.Split(stdout, "\n")
    +	for _, line := range lines {
    +		if strings.Contains(line, juicefsPath) {
    +			str = line
    +			break
    +		}
    +	}
     	// [<Filesystem>       <Size>  <Used> <Avail> <Use>% <Mounted on>]
    -	str := strings.TrimSuffix(stdout, "\n")
    -
     	data := strings.Fields(str)
     	if len(data) != 6 {
     		err = fmt.Errorf("failed to parse %s in GetUsedSpace method", data)
    @@ -354,8 +359,8 @@ func (j JuiceFileUtils) QueryMetaDataInfoIntoFile(key KeyOfMetaDataFile, filenam
     		j.log.Error(errors.New("the key not in  metadatafile"), "key", key)
     	}
     	var (
    -		str     = "sed -n '" + line + "' " + filename
    -		command = []string{"bash", "-c", str}
    +		str     = "'" + line + "' " + filename
    +		command = []string{"sed", "-n", str}
     		stdout  string
     		stderr  string
     	)
    
  • pkg/ddc/juicefs/operations/base_test.go+2 2 modified
    @@ -447,7 +447,7 @@ func TestJuiceFileUtils_GetUsedSpace(t *testing.T) {
     		t.Fatal(err.Error())
     	}
     	a := &JuiceFileUtils{log: fake.NullLogger()}
    -	_, err = a.GetUsedSpace("/tmp")
    +	_, err = a.GetUsedSpace("/runtime-mnt/juicefs/kube-system/jfsdemo/juicefs-fuse")
     	if err == nil {
     		t.Error("check failure, want err, got nil")
     	}
    @@ -457,7 +457,7 @@ func TestJuiceFileUtils_GetUsedSpace(t *testing.T) {
     	if err != nil {
     		t.Fatal(err.Error())
     	}
    -	usedSpace, err := a.GetUsedSpace("/tmp")
    +	usedSpace, err := a.GetUsedSpace("/runtime-mnt/juicefs/kube-system/jfsdemo/juicefs-fuse")
     	if err != nil {
     		t.Errorf("check failure, want nil, got err: %v", err)
     	}
    
  • pkg/ddc/juicefs/transform_fuse.go+30 17 modified
    @@ -28,6 +28,7 @@ import (
     	datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
     	"github.com/fluid-cloudnative/fluid/pkg/common"
     	"github.com/fluid-cloudnative/fluid/pkg/utils"
    +	"github.com/fluid-cloudnative/fluid/pkg/utils/security"
     )
     
     func (j *JuiceFSEngine) transformFuse(runtime *datav1alpha1.JuiceFSRuntime, dataset *datav1alpha1.Dataset, value *JuiceFS) (err error) {
    @@ -36,7 +37,7 @@ func (j *JuiceFSEngine) transformFuse(runtime *datav1alpha1.JuiceFSRuntime, data
     	}
     	mount := dataset.Spec.Mounts[0]
     
    -	value.Configs.Name = mount.Name
    +	value.Configs.Name = security.EscapeBashStr(mount.Name)
     
     	// transform image
     	image := runtime.Spec.Fuse.Image
    @@ -129,7 +130,7 @@ func (j *JuiceFSEngine) transformFuseNodeSelector(runtime *datav1alpha1.JuiceFSR
     func (j *JuiceFSEngine) genValue(mount datav1alpha1.Mount, tiredStoreLevel *datav1alpha1.Level, value *JuiceFS,
     	sharedOptions map[string]string, sharedEncryptOptions []datav1alpha1.EncryptOption) (map[string]string, error) {
     	options := make(map[string]string)
    -	value.Configs.Name = mount.Name
    +	value.Configs.Name = security.EscapeBashStr(mount.Name)
     	value.Configs.EncryptEnvOptions = make([]EncryptEnvOption, 0)
     	source := ""
     
    @@ -238,7 +239,7 @@ func (j *JuiceFSEngine) genValue(mount datav1alpha1.Mount, tiredStoreLevel *data
     	}
     
     	if source == "" {
    -		source = mount.Name
    +		source = security.EscapeBashStr(mount.Name)
     	}
     
     	// transform source
    @@ -355,7 +356,13 @@ func (j *JuiceFSEngine) genFuseMount(value *JuiceFS, optionMap map[string]string
     			}
     			optionMap["metrics"] = fmt.Sprintf("0.0.0.0:%d", metricsPort)
     		}
    -		mountArgs = []string{common.JuiceFSCeMountPath, value.Source, value.Fuse.MountPath, "-o", strings.Join(genArgs(optionMap), ",")}
    +		mountArgs = []string{
    +			common.JuiceFSCeMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Fuse.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genArgs(optionMap), ",")),
    +		}
     	} else {
     		if readonly {
     			optionMap["attrcacheto"] = "7200"
    @@ -374,11 +381,17 @@ func (j *JuiceFSEngine) genFuseMount(value *JuiceFS, optionMap map[string]string
     		optionMap["cache-group"] = cacheGroup
     		optionMap["no-sharing"] = ""
     
    -		mountArgs = []string{common.JuiceFSMountPath, value.Source, value.Fuse.MountPath, "-o", strings.Join(genArgs(optionMap), ",")}
    +		mountArgs = []string{
    +			common.JuiceFSMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Fuse.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genArgs(optionMap), ",")),
    +		}
     	}
     
     	value.Fuse.Command = strings.Join(mountArgs, " ")
    -	value.Fuse.StatCmd = "stat -c %i " + value.Fuse.MountPath
    +	value.Fuse.StatCmd = "stat -c %i " + security.EscapeBashStr(value.Fuse.MountPath)
     	return nil
     }
     
    @@ -408,7 +421,7 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string, options m
     		for _, option := range *config {
     			o := strings.TrimSpace(option)
     			if o != "" {
    -				args = append(args, fmt.Sprintf("--%s", o))
    +				args = append(args, fmt.Sprintf("--%s", security.EscapeBashStr(o)))
     			}
     		}
     	}
    @@ -424,20 +437,20 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string, options m
     			args = append(args, "--no-update")
     		}
     		if value.Configs.Storage != "" {
    -			args = append(args, fmt.Sprintf("--storage=%s", value.Configs.Storage))
    +			args = append(args, fmt.Sprintf("--storage=%s", security.EscapeBashStr(value.Configs.Storage)))
     		}
     		if value.Configs.Bucket != "" {
    -			args = append(args, fmt.Sprintf("--bucket=%s", value.Configs.Bucket))
    +			args = append(args, fmt.Sprintf("--bucket=%s", security.EscapeBashStr(value.Configs.Bucket)))
     		}
     		formatOpts := ceFilter.filterOption(options)
     		for k, v := range formatOpts {
    -			args = append(args, fmt.Sprintf("--%s=%s", k, v))
    +			args = append(args, fmt.Sprintf("--%s=%s", security.EscapeBashStr(k), security.EscapeBashStr(v)))
     		}
     		encryptOptions := ceFilter.filterEncryptEnvOptions(value.Configs.EncryptEnvOptions)
     		for _, v := range encryptOptions {
    -			args = append(args, fmt.Sprintf("--%s=${%s}", v.Name, v.EnvName))
    +			args = append(args, fmt.Sprintf("--%s=${%s}", security.EscapeBashStr(v.Name), v.EnvName))
     		}
    -		args = append(args, value.Source, value.Configs.Name)
    +		args = append(args, value.Source, security.EscapeBashStr(value.Configs.Name))
     		cmd := append([]string{common.JuiceCeCliPath, "format"}, args...)
     		value.Configs.FormatCmd = strings.Join(cmd, " ")
     		return
    @@ -455,15 +468,15 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string, options m
     		args = append(args, "--secretkey=${SECRET_KEY}")
     	}
     	if value.Configs.Bucket != "" {
    -		args = append(args, fmt.Sprintf("--bucket=%s", value.Configs.Bucket))
    +		args = append(args, fmt.Sprintf("--bucket=%s", security.EscapeBashStr(value.Configs.Bucket)))
     	}
     	formatOpts := eeFilter.filterOption(options)
     	for k, v := range formatOpts {
    -		args = append(args, fmt.Sprintf("--%s=%s", k, v))
    +		args = append(args, fmt.Sprintf("--%s=%s", security.EscapeBashStr(k), security.EscapeBashStr(v)))
     	}
     	encryptOptions := eeFilter.filterEncryptEnvOptions(value.Configs.EncryptEnvOptions)
     	for _, v := range encryptOptions {
    -		args = append(args, fmt.Sprintf("--%s=${%s}", v.Name, v.EnvName))
    +		args = append(args, fmt.Sprintf("--%s=${%s}", security.EscapeBashStr(v.Name), v.EnvName))
     	}
     	args = append(args, value.Source)
     	cmd := append([]string{common.JuiceCliPath, "auth"}, args...)
    @@ -499,13 +512,13 @@ func (j *JuiceFSEngine) genQuotaCmd(value *JuiceFS, mount datav1alpha1.Mount) er
     			if value.Edition == CommunityEdition {
     				// ce
     				// juicefs quota set ${metaurl} --path ${path} --capacity ${capacity}
    -				value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", common.JuiceCeCliPath, value.Source, value.Fuse.SubPath, qs)
    +				value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", common.JuiceCeCliPath, value.Source, security.EscapeBashStr(value.Fuse.SubPath), qs)
     				return nil
     			}
     			// ee
     			// juicefs quota set ${metaurl} --path ${path} --capacity ${capacity}
     			cli := common.JuiceCliPath
    -			value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", cli, value.Source, value.Fuse.SubPath, qs)
    +			value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", cli, value.Source, security.EscapeBashStr(value.Fuse.SubPath), qs)
     			return nil
     		}
     	}
    
  • pkg/ddc/juicefs/transform.go+17 4 modified
    @@ -28,6 +28,7 @@ import (
     	"github.com/fluid-cloudnative/fluid/pkg/ddc/base/portallocator"
     	"github.com/fluid-cloudnative/fluid/pkg/utils"
     	"github.com/fluid-cloudnative/fluid/pkg/utils/docker"
    +	"github.com/fluid-cloudnative/fluid/pkg/utils/security"
     	"github.com/fluid-cloudnative/fluid/pkg/utils/transfromer"
     )
     
    @@ -202,26 +203,38 @@ func (j *JuiceFSEngine) genWorkerMount(value *JuiceFS, workerOptionMap map[strin
     			}
     			workerOptionMap["metrics"] = fmt.Sprintf("0.0.0.0:%d", metricsPort)
     		}
    -		mountArgsWorker = []string{common.JuiceFSCeMountPath, value.Source, value.Worker.MountPath, "-o", strings.Join(genArgs(workerOptionMap), ",")}
    +		mountArgsWorker = []string{
    +			common.JuiceFSCeMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Worker.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genArgs(workerOptionMap), ",")),
    +		}
     	} else {
     		workerOptionMap["foreground"] = ""
     		// do not update config again
     		workerOptionMap["no-update"] = ""
     
     		// start independent cache cluster, refer to [juicefs cache sharing](https://juicefs.com/docs/cloud/cache/#client_cache_sharing)
     		// fuse and worker use the same cache-group, fuse use no-sharing
    -		cacheGroup := fmt.Sprintf("%s-%s", j.namespace, value.FullnameOverride)
    +		cacheGroup := fmt.Sprintf("%s-%s", j.namespace, security.EscapeBashStr(value.FullnameOverride))
     		if _, ok := workerOptionMap["cache-group"]; ok {
     			cacheGroup = workerOptionMap["cache-group"]
     		}
     		workerOptionMap["cache-group"] = cacheGroup
     		delete(workerOptionMap, "no-sharing")
     
    -		mountArgsWorker = []string{common.JuiceFSMountPath, value.Source, value.Worker.MountPath, "-o", strings.Join(genArgs(workerOptionMap), ",")}
    +		mountArgsWorker = []string{
    +			common.JuiceFSMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Worker.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genArgs(workerOptionMap), ",")),
    +		}
     	}
     
     	value.Worker.Command = strings.Join(mountArgsWorker, " ")
    -	value.Worker.StatCmd = "stat -c %i " + value.Worker.MountPath
    +	value.Worker.StatCmd = "stat -c %i " + security.EscapeBashStr(value.Worker.MountPath)
     }
     
     func (j *JuiceFSEngine) transformPlacementMode(dataset *datav1alpha1.Dataset, value *JuiceFS) {
    
  • pkg/ddc/juicefs/ufs_test.go+5 5 modified
    @@ -38,15 +38,15 @@ func mockExecCommandInContainerForTotalFileNums() (stdout string, stderr string,
     }
     
     func mockExecCommandInContainerForUsedStorageBytes() (stdout string, stderr string, err error) {
    -	r := `JuiceFS:test 207300683100160  41460043776 207259223056384   1% /data`
    +	r := `JuiceFS:test 207300683100160  41460043776 207259223056384   1% /juicefs/juicefs/test/juicefs-fuse`
     	return r, "", nil
     }
     
     func TestTotalStorageBytes(t *testing.T) {
     	statefulSet := &appsv1.StatefulSet{
     		ObjectMeta: metav1.ObjectMeta{
     			Name:      "test-worker",
    -			Namespace: "fluid",
    +			Namespace: "juicefs",
     		},
     		Spec: appsv1.StatefulSetSpec{
     			Selector: &metav1.LabelSelector{
    @@ -57,7 +57,7 @@ func TestTotalStorageBytes(t *testing.T) {
     	var pod = &corev1.Pod{
     		ObjectMeta: metav1.ObjectMeta{
     			Name:      "test-work-0",
    -			Namespace: "fluid",
    +			Namespace: "juicefs",
     			Labels:    map[string]string{"a": "b"},
     		},
     		Status: corev1.PodStatus{
    @@ -93,11 +93,11 @@ func TestTotalStorageBytes(t *testing.T) {
     			name: "test",
     			fields: fields{
     				name:      "test",
    -				namespace: "fluid",
    +				namespace: "juicefs",
     				runtime: &datav1alpha1.JuiceFSRuntime{
     					ObjectMeta: metav1.ObjectMeta{
     						Name:      "test",
    -						Namespace: "fluid",
    +						Namespace: "juicefs",
     					},
     				},
     			},
    
  • pkg/utils/security/escape.go+63 0 added
    @@ -0,0 +1,63 @@
    +/*
    +Copyright 2023 The Fluid Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package security
    +
    +import (
    +	"fmt"
    +	"strings"
    +)
    +
    +// According to https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html#ANSI_002dC-Quoting
    +// a -> a
    +// a b -> a b
    +// $a -> $'$a'
    +// $'a' -> $'$\'$a'\'
    +func EscapeBashStr(s string) string {
    +	if !containsOne(s, []rune{'$', '`', '&', ';', '>', '|', '(', ')'}) {
    +		return s
    +	}
    +	s = strings.ReplaceAll(s, `\`, `\\`)
    +	s = strings.ReplaceAll(s, `'`, `\'`)
    +	if strings.Contains(s, `\\`) {
    +		s = strings.ReplaceAll(s, `\\\\`, `\\`)
    +		s = strings.ReplaceAll(s, `\\\'`, `\'`)
    +		s = strings.ReplaceAll(s, `\\"`, `\"`)
    +		s = strings.ReplaceAll(s, `\\a`, `\a`)
    +		s = strings.ReplaceAll(s, `\\b`, `\b`)
    +		s = strings.ReplaceAll(s, `\\e`, `\e`)
    +		s = strings.ReplaceAll(s, `\\E`, `\E`)
    +		s = strings.ReplaceAll(s, `\\n`, `\n`)
    +		s = strings.ReplaceAll(s, `\\r`, `\r`)
    +		s = strings.ReplaceAll(s, `\\t`, `\t`)
    +		s = strings.ReplaceAll(s, `\\v`, `\v`)
    +		s = strings.ReplaceAll(s, `\\?`, `\?`)
    +	}
    +	return fmt.Sprintf(`$'%s'`, s)
    +}
    +
    +func containsOne(target string, chars []rune) bool {
    +	charMap := make(map[rune]bool, len(chars))
    +	for _, c := range chars {
    +		charMap[c] = true
    +	}
    +	for _, s := range target {
    +		if charMap[s] {
    +			return true
    +		}
    +	}
    +	return false
    +}
    
  • pkg/utils/security/escape_test.go+41 0 added
    @@ -0,0 +1,41 @@
    +/*
    +Copyright 2023 The Fluid Author.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package security
    +
    +import "testing"
    +
    +func TestEscapeBashStr(t *testing.T) {
    +	cases := [][]string{
    +		{"abc", "abc"},
    +		{"test-volume", "test-volume"},
    +		{"http://minio.kube-system:9000/minio/dynamic-ce", "http://minio.kube-system:9000/minio/dynamic-ce"},
    +		{"$(cat /proc/self/status | grep CapEff > /test.txt)", "$'$(cat /proc/self/status | grep CapEff > /test.txt)'"},
    +		{"hel`cat /proc/self/status`lo", "$'hel`cat /proc/self/status`lo'"},
    +		{"'h'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"\\'h\\'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"$'h'el`cat /proc/self/status`lo", "$'$\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"hel\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
    +		{"hel\\\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
    +		{"hel\\'`cat /proc/self/status`lo", "$'hel\\'`cat /proc/self/status`lo'"},
    +	}
    +	for _, c := range cases {
    +		escaped := EscapeBashStr(c[0])
    +		if escaped != c[1] {
    +			t.Errorf("escapeBashVar(%s) = %s, want %s", c[0], escaped, c[1])
    +		}
    +	}
    +}
    
e0184cff8790

Merge pull request from GHSA-wx8q-4gm9-rj2g

7 files changed · +166 32
  • charts/juicefs/Chart.yaml+1 1 modified
    @@ -1,7 +1,7 @@
     name: juicefs
     apiVersion: v1
     description: FileSystem aimed for data analytics and machine learning in any cloud.
    -version: 0.2.14
    +version: 0.2.16
     appVersion: v1.0.0
     home: https://juicefs.com/
     maintainers:
    
  • pkg/ddc/juicefs/operations/base.go+14 9 modified
    @@ -27,6 +27,7 @@ import (
     	"github.com/go-logr/logr"
     
     	"github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient"
    +	"github.com/fluid-cloudnative/fluid/pkg/utils/security"
     )
     
     type JuiceFileUtils struct {
    @@ -130,12 +131,11 @@ func (j JuiceFileUtils) Count(juiceSubPath string) (total int64, err error) {
     func (j JuiceFileUtils) GetFileCount(juiceSubPath string) (fileCount int64, err error) {
     	var (
     		//strs    = "du -ah juiceSubPath |grep ^- |wc -l "
    -		strs    = fmt.Sprintf("ls -lR %s |grep ^- |wc -l ", juiceSubPath)
    +		strs    = fmt.Sprintf("ls -lR %s |grep ^- |wc -l ", security.EscapeBashStr(juiceSubPath))
     		command = []string{"bash", "-c", strs}
     		stdout  string
     		stderr  string
     	)
    -
     	stdout, stderr, err = j.exec(command)
     	if err != nil {
     		err = fmt.Errorf("execute command %v with expectedErr: %v stdout %s and stderr %s", command, err, stdout, stderr)
    @@ -266,11 +266,10 @@ func (j JuiceFileUtils) GetMetric(juicefsPath string) (metrics string, err error
     }
     
     // GetUsedSpace Get used space in byte
    -// use "df --block-size=1 |grep <juicefsPath>'"
    +// equal to `df --block-size=1 | grep juicefsPath`
     func (j JuiceFileUtils) GetUsedSpace(juicefsPath string) (usedSpace int64, err error) {
     	var (
    -		strs    = fmt.Sprintf(`df --block-size=1 |grep %s`, juicefsPath)
    -		command = []string{"bash", "-c", strs}
    +		command = []string{"df", "--block-size=1"}
     		stdout  string
     		stderr  string
     	)
    @@ -281,9 +280,15 @@ func (j JuiceFileUtils) GetUsedSpace(juicefsPath string) (usedSpace int64, err e
     		return
     	}
     
    +	var str string
    +	lines := strings.Split(stdout, "\n")
    +	for _, line := range lines {
    +		if strings.Contains(line, juicefsPath) {
    +			str = line
    +			break
    +		}
    +	}
     	// [<Filesystem>       <Size>  <Used> <Avail> <Use>% <Mounted on>]
    -	str := strings.TrimSuffix(stdout, "\n")
    -
     	data := strings.Fields(str)
     	if len(data) != 6 {
     		err = fmt.Errorf("failed to parse %s in GetUsedSpace method", data)
    @@ -365,8 +370,8 @@ func (j JuiceFileUtils) QueryMetaDataInfoIntoFile(key KeyOfMetaDataFile, filenam
     		j.log.Error(errors.New("the key not in  metadatafile"), "key", key)
     	}
     	var (
    -		str     = "sed -n '" + line + "' " + filename
    -		command = []string{"bash", "-c", str}
    +		str     = "'" + line + "' " + filename
    +		command = []string{"sed", "-n", str}
     		stdout  string
     		stderr  string
     	)
    
  • pkg/ddc/juicefs/operations/base_test.go+2 2 modified
    @@ -479,7 +479,7 @@ func TestJuiceFileUtils_GetUsedSpace(t *testing.T) {
     		t.Fatal(err.Error())
     	}
     	a := &JuiceFileUtils{log: fake.NullLogger()}
    -	_, err = a.GetUsedSpace("/tmp")
    +	_, err = a.GetUsedSpace("/runtime-mnt/juicefs/kube-system/jfsdemo/juicefs-fuse")
     	if err == nil {
     		t.Error("check failure, want err, got nil")
     	}
    @@ -489,7 +489,7 @@ func TestJuiceFileUtils_GetUsedSpace(t *testing.T) {
     	if err != nil {
     		t.Fatal(err.Error())
     	}
    -	usedSpace, err := a.GetUsedSpace("/tmp")
    +	usedSpace, err := a.GetUsedSpace("/runtime-mnt/juicefs/kube-system/jfsdemo/juicefs-fuse")
     	if err != nil {
     		t.Errorf("check failure, want nil, got err: %v", err)
     	}
    
  • pkg/ddc/juicefs/transform_fuse.go+40 15 modified
    @@ -29,6 +29,7 @@ import (
     	"github.com/fluid-cloudnative/fluid/pkg/common"
     	"github.com/fluid-cloudnative/fluid/pkg/utils"
     	"github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient"
    +	"github.com/fluid-cloudnative/fluid/pkg/utils/security"
     )
     
     func (j *JuiceFSEngine) transformFuse(runtime *datav1alpha1.JuiceFSRuntime, dataset *datav1alpha1.Dataset, value *JuiceFS) (err error) {
    @@ -37,7 +38,7 @@ func (j *JuiceFSEngine) transformFuse(runtime *datav1alpha1.JuiceFSRuntime, data
     	}
     	mount := dataset.Spec.Mounts[0]
     
    -	value.Configs.Name = mount.Name
    +	value.Configs.Name = security.EscapeBashStr(mount.Name)
     
     	// transform image
     	image := runtime.Spec.Fuse.Image
    @@ -216,7 +217,7 @@ func (j *JuiceFSEngine) genValue(mount datav1alpha1.Mount, tiredStoreLevel *data
     	}
     
     	if source == "" {
    -		source = mount.Name
    +		source = security.EscapeBashStr(mount.Name)
     	}
     
     	// transform source
    @@ -326,8 +327,20 @@ func (j *JuiceFSEngine) genMount(value *JuiceFS, runtime *datav1alpha1.JuiceFSRu
     			}
     			workerOptionMap["metrics"] = fmt.Sprintf("0.0.0.0:%d", metricsPort)
     		}
    -		mountArgs = []string{common.JuiceFSCeMountPath, value.Source, value.Fuse.MountPath, "-o", strings.Join(genOption(optionMap), ",")}
    -		mountArgsWorker = []string{common.JuiceFSCeMountPath, value.Source, value.Worker.MountPath, "-o", strings.Join(genOption(workerOptionMap), ",")}
    +		mountArgs = []string{
    +			common.JuiceFSCeMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Fuse.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genOption(optionMap), ",")),
    +		}
    +		mountArgsWorker = []string{
    +			common.JuiceFSCeMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Worker.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genOption(workerOptionMap), ",")),
    +		}
     	} else {
     		if readonly {
     			optionMap["attrcacheto"] = "7200"
    @@ -347,14 +360,26 @@ func (j *JuiceFSEngine) genMount(value *JuiceFS, runtime *datav1alpha1.JuiceFSRu
     		optionMap["no-sharing"] = ""
     		delete(workerOptionMap, "no-sharing")
     
    -		mountArgs = []string{common.JuiceFSMountPath, value.Source, value.Fuse.MountPath, "-o", strings.Join(genOption(optionMap), ",")}
    -		mountArgsWorker = []string{common.JuiceFSMountPath, value.Source, value.Worker.MountPath, "-o", strings.Join(genOption(workerOptionMap), ",")}
    +		mountArgs = []string{
    +			common.JuiceFSMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Fuse.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genOption(optionMap), ",")),
    +		}
    +		mountArgsWorker = []string{
    +			common.JuiceFSMountPath,
    +			value.Source,
    +			security.EscapeBashStr(value.Worker.MountPath),
    +			"-o",
    +			security.EscapeBashStr(strings.Join(genOption(workerOptionMap), ",")),
    +		}
     	}
     
     	value.Worker.Command = strings.Join(mountArgsWorker, " ")
     	value.Fuse.Command = strings.Join(mountArgs, " ")
    -	value.Fuse.StatCmd = "stat -c %i " + value.Fuse.MountPath
    -	value.Worker.StatCmd = "stat -c %i " + value.Worker.MountPath
    +	value.Fuse.StatCmd = "stat -c %i " + security.EscapeBashStr(value.Fuse.MountPath)
    +	value.Worker.StatCmd = "stat -c %i " + security.EscapeBashStr(value.Worker.MountPath)
     	return nil
     }
     
    @@ -379,7 +404,7 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string) {
     		for _, option := range *config {
     			o := strings.TrimSpace(option)
     			if o != "" {
    -				args = append(args, fmt.Sprintf("--%s", o))
    +				args = append(args, fmt.Sprintf("--%s", security.EscapeBashStr(o)))
     			}
     		}
     	}
    @@ -395,12 +420,12 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string) {
     			args = append(args, "--no-update")
     		}
     		if value.Configs.Storage != "" {
    -			args = append(args, fmt.Sprintf("--storage=%s", value.Configs.Storage))
    +			args = append(args, fmt.Sprintf("--storage=%s", security.EscapeBashStr(value.Configs.Storage)))
     		}
     		if value.Configs.Bucket != "" {
    -			args = append(args, fmt.Sprintf("--bucket=%s", value.Configs.Bucket))
    +			args = append(args, fmt.Sprintf("--bucket=%s", security.EscapeBashStr(value.Configs.Bucket)))
     		}
    -		args = append(args, value.Source, value.Configs.Name)
    +		args = append(args, value.Source, security.EscapeBashStr(value.Configs.Name))
     		cmd := append([]string{common.JuiceCeCliPath, "format"}, args...)
     		value.Configs.FormatCmd = strings.Join(cmd, " ")
     		return
    @@ -418,7 +443,7 @@ func (j *JuiceFSEngine) genFormatCmd(value *JuiceFS, config *[]string) {
     		args = append(args, "--secretkey=${SECRET_KEY}")
     	}
     	if value.Configs.Bucket != "" {
    -		args = append(args, fmt.Sprintf("--bucket=%s", value.Configs.Bucket))
    +		args = append(args, fmt.Sprintf("--bucket=%s", security.EscapeBashStr(value.Configs.Bucket)))
     	}
     	args = append(args, value.Source)
     	cmd := append([]string{common.JuiceCliPath, "auth"}, args...)
    @@ -461,7 +486,7 @@ func (j *JuiceFSEngine) genQuotaCmd(value *JuiceFS, mount datav1alpha1.Mount) er
     					return fmt.Errorf("quota is not supported in juicefs-ce version %s", value.Fuse.ImageTag)
     				}
     				// juicefs quota set ${metaurl} --path ${path} --capacity ${capacity}
    -				value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", common.JuiceCeCliPath, value.Source, value.Fuse.SubPath, qs)
    +				value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", common.JuiceCeCliPath, value.Source, security.EscapeBashStr(value.Fuse.SubPath), qs)
     				return nil
     			}
     			// ee
    @@ -470,7 +495,7 @@ func (j *JuiceFSEngine) genQuotaCmd(value *JuiceFS, mount datav1alpha1.Mount) er
     			}
     			// juicefs quota set ${metaurl} --path ${path} --capacity ${capacity}
     			cli := common.JuiceCliPath
    -			value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", cli, value.Source, value.Fuse.SubPath, qs)
    +			value.Configs.QuotaCmd = fmt.Sprintf("%s quota set %s --path %s --capacity %d", cli, value.Source, security.EscapeBashStr(value.Fuse.SubPath), qs)
     			return nil
     		}
     	}
    
  • pkg/ddc/juicefs/ufs_test.go+5 5 modified
    @@ -38,15 +38,15 @@ func mockExecCommandInContainerForTotalFileNums() (stdout string, stderr string,
     }
     
     func mockExecCommandInContainerForUsedStorageBytes() (stdout string, stderr string, err error) {
    -	r := `JuiceFS:test 207300683100160  41460043776 207259223056384   1% /data`
    +	r := `JuiceFS:test 207300683100160  41460043776 207259223056384   1% /juicefs/juicefs/test/juicefs-fuse`
     	return r, "", nil
     }
     
     func TestTotalStorageBytes(t *testing.T) {
     	statefulSet := &appsv1.StatefulSet{
     		ObjectMeta: metav1.ObjectMeta{
     			Name:      "test-worker",
    -			Namespace: "fluid",
    +			Namespace: "juicefs",
     		},
     		Spec: appsv1.StatefulSetSpec{
     			Selector: &metav1.LabelSelector{
    @@ -57,7 +57,7 @@ func TestTotalStorageBytes(t *testing.T) {
     	var pod = &corev1.Pod{
     		ObjectMeta: metav1.ObjectMeta{
     			Name:      "test-work-0",
    -			Namespace: "fluid",
    +			Namespace: "juicefs",
     			Labels:    map[string]string{"a": "b"},
     		},
     		Status: corev1.PodStatus{
    @@ -93,11 +93,11 @@ func TestTotalStorageBytes(t *testing.T) {
     			name: "test",
     			fields: fields{
     				name:      "test",
    -				namespace: "fluid",
    +				namespace: "juicefs",
     				runtime: &datav1alpha1.JuiceFSRuntime{
     					ObjectMeta: metav1.ObjectMeta{
     						Name:      "test",
    -						Namespace: "fluid",
    +						Namespace: "juicefs",
     					},
     				},
     			},
    
  • pkg/utils/security/escape.go+63 0 added
    @@ -0,0 +1,63 @@
    +/*
    +Copyright 2023 The Fluid Authors.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package security
    +
    +import (
    +	"fmt"
    +	"strings"
    +)
    +
    +// According to https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html#ANSI_002dC-Quoting
    +// a -> a
    +// a b -> a b
    +// $a -> $'$a'
    +// $'a' -> $'$\'$a'\'
    +func EscapeBashStr(s string) string {
    +	if !containsOne(s, []rune{'$', '`', '&', ';', '>', '|', '(', ')'}) {
    +		return s
    +	}
    +	s = strings.ReplaceAll(s, `\`, `\\`)
    +	s = strings.ReplaceAll(s, `'`, `\'`)
    +	if strings.Contains(s, `\\`) {
    +		s = strings.ReplaceAll(s, `\\\\`, `\\`)
    +		s = strings.ReplaceAll(s, `\\\'`, `\'`)
    +		s = strings.ReplaceAll(s, `\\"`, `\"`)
    +		s = strings.ReplaceAll(s, `\\a`, `\a`)
    +		s = strings.ReplaceAll(s, `\\b`, `\b`)
    +		s = strings.ReplaceAll(s, `\\e`, `\e`)
    +		s = strings.ReplaceAll(s, `\\E`, `\E`)
    +		s = strings.ReplaceAll(s, `\\n`, `\n`)
    +		s = strings.ReplaceAll(s, `\\r`, `\r`)
    +		s = strings.ReplaceAll(s, `\\t`, `\t`)
    +		s = strings.ReplaceAll(s, `\\v`, `\v`)
    +		s = strings.ReplaceAll(s, `\\?`, `\?`)
    +	}
    +	return fmt.Sprintf(`$'%s'`, s)
    +}
    +
    +func containsOne(target string, chars []rune) bool {
    +	charMap := make(map[rune]bool, len(chars))
    +	for _, c := range chars {
    +		charMap[c] = true
    +	}
    +	for _, s := range target {
    +		if charMap[s] {
    +			return true
    +		}
    +	}
    +	return false
    +}
    
  • pkg/utils/security/escape_test.go+41 0 added
    @@ -0,0 +1,41 @@
    +/*
    +Copyright 2023 The Fluid Author.
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +    http://www.apache.org/licenses/LICENSE-2.0
    +
    +Unless required by applicable law or agreed to in writing, software
    +distributed under the License is distributed on an "AS IS" BASIS,
    +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +See the License for the specific language governing permissions and
    +limitations under the License.
    +*/
    +
    +package security
    +
    +import "testing"
    +
    +func TestEscapeBashStr(t *testing.T) {
    +	cases := [][]string{
    +		{"abc", "abc"},
    +		{"test-volume", "test-volume"},
    +		{"http://minio.kube-system:9000/minio/dynamic-ce", "http://minio.kube-system:9000/minio/dynamic-ce"},
    +		{"$(cat /proc/self/status | grep CapEff > /test.txt)", "$'$(cat /proc/self/status | grep CapEff > /test.txt)'"},
    +		{"hel`cat /proc/self/status`lo", "$'hel`cat /proc/self/status`lo'"},
    +		{"'h'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"\\'h\\'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"$'h'el`cat /proc/self/status`lo", "$'$\\'h\\'el`cat /proc/self/status`lo'"},
    +		{"hel\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
    +		{"hel\\\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
    +		{"hel\\'`cat /proc/self/status`lo", "$'hel\\'`cat /proc/self/status`lo'"},
    +	}
    +	for _, c := range cases {
    +		escaped := EscapeBashStr(c[0])
    +		if escaped != c[1] {
    +			t.Errorf("escapeBashVar(%s) = %s, want %s", c[0], escaped, c[1])
    +		}
    +	}
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.