-
Notifications
You must be signed in to change notification settings - Fork 1.3k
optim(utils): sanitize and clamp subpaths to handle path escaping #6159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70d75f1
6432fdf
076f0ba
5b64036
273fc0c
6f66e50
55ca455
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,6 @@ limitations under the License. | |
| package poststart | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
|
|
@@ -73,7 +72,7 @@ fi | |
|
|
||
| count=1 | ||
| limit=30 | ||
| while ! cat /proc/self/mountinfo | grep $ConditionPathIsMountPoint | grep $MountType | ||
| while ! cat /proc/self/mountinfo | grep -F "$ConditionPathIsMountPoint" | grep -F "$MountType" | ||
| do | ||
| sleep 1 | ||
| count=¬expr $count + 1¬ | ||
|
|
@@ -87,7 +86,7 @@ done | |
| # different with csi, as here the mount point is the parent dir of the fuse mount point, | ||
| subpath_check_count=1 | ||
| subpath_check_limit=30 | ||
| while [ ! -e $ConditionPathIsMountPoint/*/$SubPath ] | ||
| while ! ls -d "$ConditionPathIsMountPoint"/*/"$SubPath" >/dev/null 2>&1 | ||
| do | ||
| sleep 1 | ||
| subpath_check_count=¬expr $subpath_check_count + 1¬ | ||
|
|
@@ -129,7 +128,9 @@ func NewDefaultPostStartScriptGenerator() *defaultPostStartScriptGenerator { | |
|
|
||
| func (g *defaultPostStartScriptGenerator) GetPostStartCommand(mountPath, mountType, subPath string) (handler *corev1.LifecycleHandler) { | ||
| // https://github.com/kubernetes/kubernetes/issues/25766 | ||
| cmd := []string{"bash", "-c", fmt.Sprintf("time %s %s %s %s", g.scriptMountPath, mountPath, mountType, subPath)} | ||
| // Arguments are passed as positional parameters so that user-controlled values (e.g. subPath) | ||
| // are never re-parsed by the shell. | ||
| cmd := []string{"bash", "-c", `time "$0" "$@"`, g.scriptMountPath, mountPath, mountType, subPath} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anchored here because line 89 of this file is outside the diff. That is the real spot: while [ ! -e $ConditionPathIsMountPoint/*/$SubPath ]Unquoted, plus a literal glob. A value with a space becomes extra operands, The first line is the control: the gate works normally, it only breaks on the crafted value, and that value passes Your fix moved this one rather than helped it. Quoting alone will not do it, since a glob matching two directories has the same problem. This works, crafted case exits 2 and the ordinary one stays green: while ! ls -d "$ConditionPathIsMountPoint"/*/"$SubPath" >/dev/null 2>&1
|
||
|
|
||
| return &corev1.LifecycleHandler{ | ||
| Exec: &corev1.ExecAction{Command: cmd}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,9 +134,21 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis | |
| mountType = common.AlluxioMountType | ||
| } | ||
|
|
||
| if !filepath.IsAbs(fluidPath) { | ||
| return nil, status.Errorf(codes.InvalidArgument, "%s must be an absolute path, but got \"%s\"", common.VolumeAttrFluidPath, fluidPath) | ||
| } | ||
| fluidPath = filepath.Clean(fluidPath) | ||
| if err := checkPathUnderMountRoot(common.VolumeAttrFluidPath, fluidPath); err != nil { | ||
|
TrafalgarZZZ marked this conversation as resolved.
|
||
| return nil, status.Error(codes.InvalidArgument, err.Error()) | ||
| } | ||
|
|
||
| mountPath := fluidPath | ||
| if subPath != "" { | ||
| mountPath = fluidPath + "/" + subPath | ||
| subPath, err = utils.NormalizeSubPath(subPath) | ||
| if err != nil { | ||
| return nil, status.Errorf(codes.InvalidArgument, "invalid %s: %v", common.VolumeAttrFluidSubPath, err) | ||
| } | ||
| mountPath = filepath.Join(mountPath, subPath) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The three specs added here carry real assertions on |
||
| } | ||
|
|
||
| // 1. Wait the runtime fuse ready and check the sub path existence | ||
|
|
@@ -159,6 +171,17 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis | |
| } | ||
| } | ||
|
|
||
| // 2. Resolve the bind mount source below the FUSE mount point. Every component of subPath is | ||
| // opened without following symlinks, so a symlink planted anywhere under the mount point cannot | ||
| // redirect the mount to an arbitrary path on the host. | ||
| mountSource, closeMountSource, err := resolveMountSource(fluidPath, subPath) | ||
| if err != nil { | ||
| return nil, status.Error(codes.InvalidArgument, err.Error()) | ||
| } | ||
| // The source pins the resolved inode only while the descriptor is open, so it must outlive the | ||
| // mount call below. | ||
| defer closeMountSource() | ||
|
|
||
| // use symlink | ||
| if useSymlink(req) { | ||
| if err := utils.CreateSymlink(targetPath, mountPath); err != nil { | ||
|
|
@@ -174,9 +197,9 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis | |
| // } | ||
|
|
||
| if readOnly { | ||
| args = append(args, "-o", "ro", mountPath, targetPath) | ||
| args = append(args, "-o", "ro", mountSource, targetPath) | ||
| } else { | ||
| args = append(args, mountPath, targetPath) | ||
| args = append(args, mountSource, targetPath) | ||
| } | ||
| command, err := cmdguard.Command("mount", args...) | ||
| if err != nil { | ||
|
|
@@ -392,6 +415,22 @@ func (ns *nodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetC | |
| }, nil | ||
| } | ||
|
|
||
| // checkPathUnderMountRoot rejects a path that does not live under the mount root configured via | ||
| // the MOUNT_ROOT env. The path comes from PV volume attributes, which are not under CSI's control, | ||
| // so an arbitrary host path such as "/etc" must not be accepted for mounting. | ||
| func checkPathUnderMountRoot(attrName, path string) error { | ||
| mountRoot, err := utils.GetMountRoot() | ||
| if err != nil { | ||
| return errors.Wrapf(err, "failed to get mount root for validating %s \"%s\"", attrName, path) | ||
| } | ||
|
|
||
| if !utils.IsSubPath(mountRoot, path) { | ||
| return fmt.Errorf("%s \"%s\" must be under the mount root \"%s\"", attrName, path, mountRoot) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // getRuntimeNamespacedName first checks volume context for runtime's namespace and name as a fast path. | ||
| // If not found, it takes a fallback to query API Server and to parse the PV information. | ||
| func (ns *nodeServer) getRuntimeNamespacedName(volumeContext map[string]string, volumeId string) (namespace string, name string, err error) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.