CVE-2023-25173 (Go)

containerd's OCI spec generation can produce process credentials where the current primary GID is not included in the group list passed to the runtime when user or group settings are resolved from image, OCI, CRI, or client-library configuration. Because container startup may apply the generated supplementary group list with setgroups-like behavior, omitting the primary GID can cause the container process to run with an unintended set of group credentials: the primary group may be dropped while other supplementary groups remain. An attacker who can influence the container user/group configuration or has direct access to the resulting container may abuse this incorrect group setup to bypass expected group-based access restrictions, potentially gaining access to sensitive files or execution capabilities inside the container.

Weakness:
CWE-285: name: Improper Authorization
description: The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.
CWE-863: name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
CWE-250: name: Execution with Unnecessary Privileges
description: The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.
CWE-269: name: Improper Privilege Management
description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

Repository: https://github.com/containerd/containerd

Vulnerable code:
file_path: oci/spec_opts.go
snippet: func WithUser(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
		setProcess(s)

		// For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't
		// mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the
		// guest itself. To accommodate this, a spot to place the user string provided by a client as-is is needed.
		// The `Username` field on the runtime spec is marked by Platform as only for Windows, and in this case it
		// *is* being set on a Windows host at least, but will be used as a temporary holding spot until the guest
		// can use the string to perform these same operations to grab the uid:gid inside.
		if s.Windows != nil && s.Linux != nil {
			s.Process.User.Username = userstr
			return nil
		}

		parts := strings.Split(userstr, ":")
		switch len(parts) {
		case 1:
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				// if we cannot parse as a uint they try to see if it is a username
				return WithUsername(userstr)(ctx, client, c, s)
			}
			return WithUserID(uint32(v))(ctx, client, c, s)
		case 2:
			var (
				username  string
				groupname string
			)
			var uid, gid uint32
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				username = parts[0]
			} else {
				uid = uint32(v)
			}
			if v, err = strconv.Atoi(parts[1]); err != nil {
				groupname = parts[1]
			} else {
				gid = uint32(v)
			}
			if username == "" && groupname == "" {
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			f := func(root string) error {
				if username != "" {
					user, err := UserFromPath(root, func(u user.User) bool {
						return u.Name == username
					})
					if err != nil {
						return err
					}
					uid = uint32(user.Uid)
				}
				if groupname != "" {
					gid, err = GIDFromPath(root, func(g user.Group) bool {
						return g.Name == groupname
					})
					if err != nil {
						return err
					}
				}
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return f(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, f)
		default:
			return fmt.Errorf("invalid USER value %s", userstr)
		}
	}
}

file_path: oci/spec_opts.go
snippet: func WithUIDGID(uid, gid uint32) SpecOpts {
	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
		setProcess(s)
		s.Process.User.UID = uid
		s.Process.User.GID = gid
		return nil
	}
}

file_path: oci/spec_opts.go
snippet: func WithUserID(uid uint32) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		setProcess(s)
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil

		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, func(root string) error {
			user, err := UserFromPath(root, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil
		})
	}
}

file_path: oci/spec_opts.go
snippet: func WithUsername(username string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		setProcess(s)
		if s.Linux != nil {
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, func(root string) error {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			})
		} else if s.Windows != nil {
			s.Process.User.Username = username
		} else {
			return errors.New("spec does not contain Linux or Windows section")
		}
		return nil
	}
}

file_path: oci/spec_opts.go
snippet: func WithAdditionalGIDs(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		setAdditionalGids := func(root string) error {
			var username string
			uid, err := strconv.Atoi(userstr)
			if err == nil {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Uid == uid
				})
				if err != nil {
					if os.IsNotExist(err) || err == ErrNoUsersFound {
						return nil
					}
					return err
				}
				username = user.Name
			} else {
				username = userstr
			}
			gids, err := getSupplementalGroupsFromPath(root, func(g user.Group) bool {
				// we only want supplemental groups
				if g.Name == username {
					return false
				}
				for _, entry := range g.List {
					if entry == username {
						return true
					}
				}
				return false
			})
			if err != nil {
				if os.IsNotExist(err) {
					return nil
				}
				return err
			}
			s.Process.User.AdditionalGids = gids
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}

Fixed code:
file_path: oci/spec_opts.go
snippet: // ensureAdditionalGids ensures that the primary GID is also included in the additional GID list.
func ensureAdditionalGids(s *Spec) {
	setProcess(s)
	for _, f := range s.Process.User.AdditionalGids {
		if f == s.Process.User.GID {
			return
		}
	}
	s.Process.User.AdditionalGids = append([]uint32{s.Process.User.GID}, s.Process.User.AdditionalGids...)
}


file_path: oci/spec_opts.go
snippet: func WithUser(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil

		// For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't
		// mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the
		// guest itself. To accommodate this, a spot to place the user string provided by a client as-is is needed.
		// The `Username` field on the runtime spec is marked by Platform as only for Windows, and in this case it
		// *is* being set on a Windows host at least, but will be used as a temporary holding spot until the guest
		// can use the string to perform these same operations to grab the uid:gid inside.
		if s.Windows != nil && s.Linux != nil {
			s.Process.User.Username = userstr
			return nil
		}

		parts := strings.Split(userstr, ":")
		switch len(parts) {
		case 1:
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				// if we cannot parse as a uint they try to see if it is a username
				return WithUsername(userstr)(ctx, client, c, s)
			}
			return WithUserID(uint32(v))(ctx, client, c, s)
		case 2:
			var (
				username  string
				groupname string
			)
			var uid, gid uint32
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				username = parts[0]
			} else {
				uid = uint32(v)
			}
			if v, err = strconv.Atoi(parts[1]); err != nil {
				groupname = parts[1]
			} else {
				gid = uint32(v)
			}
			if username == "" && groupname == "" {
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			f := func(root string) error {
				if username != "" {
					user, err := UserFromPath(root, func(u user.User) bool {
						return u.Name == username
					})
					if err != nil {
						return err
					}
					uid = uint32(user.Uid)
				}
				if groupname != "" {
					gid, err = GIDFromPath(root, func(g user.Group) bool {
						return g.Name == groupname
					})
					if err != nil {
						return err
					}
				}
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return f(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, f)
		default:
			return fmt.Errorf("invalid USER value %s", userstr)
		}
	}
}

file_path: oci/spec_opts.go
snippet: func WithUIDGID(uid, gid uint32) SpecOpts {
	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		s.Process.User.UID = uid
		s.Process.User.GID = gid
		return nil
	}
}

file_path: oci/spec_opts.go
snippet: func WithUserID(uid uint32) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		setUser := func(root string) error {
			user, err := UserFromPath(root, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setUser(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setUser)
	}
}

file_path: oci/spec_opts.go
snippet: func WithUsername(username string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		if s.Linux != nil {
			setUser := func(root string) error {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return setUser(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, setUser)
		} else if s.Windows != nil {
			s.Process.User.Username = username
		} else {
			return errors.New("spec does not contain Linux or Windows section")
		}
		return nil
	}
}

file_path: oci/spec_opts.go
snippet: func WithAdditionalGIDs(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		setAdditionalGids := func(root string) error {
			defer ensureAdditionalGids(s)
			var username string
			uid, err := strconv.Atoi(userstr)
			if err == nil {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Uid == uid
				})
				if err != nil {
					if os.IsNotExist(err) || err == ErrNoUsersFound {
						return nil
					}
					return err
				}
				username = user.Name
			} else {
				username = userstr
			}
			gids, err := getSupplementalGroupsFromPath(root, func(g user.Group) bool {
				// we only want supplemental groups
				if g.Name == username {
					return false
				}
				for _, entry := range g.List {
					if entry == username {
						return true
					}
				}
				return false
			})
			if err != nil {
				if os.IsNotExist(err) {
					return nil
				}
				return err
			}
			s.Process.User.AdditionalGids = gids
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}

file_path: oci/spec_opts.go
snippet: func WithAppendAdditionalGroups(groups ...string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's are not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		setAdditionalGids := func(root string) error {
			defer ensureAdditionalGids(s)
			gpath, err := fs.RootPath(root, "/etc/group")
			if err != nil {
				return err
			}
			ugroups, err := user.ParseGroupFile(gpath)
			if err != nil {
				return err
			}
			groupMap := make(map[string]user.Group)
			for _, group := range ugroups {
				groupMap[group.Name] = group
			}
			var gids []uint32
			for _, group := range groups {
				gid, err := strconv.ParseUint(group, 10, 32)
				if err == nil {
					gids = append(gids, uint32(gid))
				} else {
					g, ok := groupMap[group]
					if !ok {
						return fmt.Errorf("unable to find group %s", group)
					}
					gids = append(gids, uint32(g.Gid))
				}
			}
			s.Process.User.AdditionalGids = append(s.Process.User.AdditionalGids, gids...)
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !filepath.IsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}