OS Command Injection for Fluid Users with JuicefsRuntime
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/fluid-cloudnative/fluidGo | < 0.9.3 | 0.9.3 |
Affected products
3<0.9.3+ 1 more
- (no CPE)range: <0.9.3
- (no CPE)range: < 0.9.3
Patches
202b7cd8b79a2Fix 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]) + } + } +}
e0184cff8790Merge 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- github.com/advisories/GHSA-wx8q-4gm9-rj2gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-51699ghsaADVISORY
- github.com/fluid-cloudnative/fluid/commit/02b7cd8b79a26092df95d625664994bda485c722ghsaWEB
- github.com/fluid-cloudnative/fluid/commit/e0184cff8790ad000c3e8943392c7f544fad7d66ghsax_refsource_MISCWEB
- github.com/fluid-cloudnative/fluid/security/advisories/GHSA-wx8q-4gm9-rj2gghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.