From 03aff6270b389f27bc1edc4985dab753a38e7c7b Mon Sep 17 00:00:00 2001 From: Matt Heon Date: Wed, 9 Oct 2024 09:54:22 -0400 Subject: [PATCH] Use securejoin.SecureJoin when forming userns paths We need to read /etc/passwd and /etc/group in the container to get an idea of how many UIDs and GIDs we need to allocate for a user namespace when `--userns=auto` is specified. We were forming paths for these using filepath.Join, which is not safe for paths within a container, resulting in this CVE allowing crafted symlinks in the container to access paths on the host instead. Cherry-pick conflict fixed for v1.51 branch, and converted to use the old securejoin API (securejoin.SecureJoin and then os.Open) as this branch is too old to have the new API. Addresses CVE-2024-9676 Signed-off-by: Matt Heon Upstream-Status: Backport [0dc4fc9bb826e08b6e25af0af6a296ac172b5e15] CVE: CVE-2024-9676 Signed-off-by: Chen Qi --- userns.go | 93 ++++++++++++++++++++++++++++++------------- userns_unsupported.go | 14 +++++++ 2 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 userns_unsupported.go diff --git a/userns.go b/userns.go index 523c92dc8..c234414ef 100644 --- a/userns.go +++ b/userns.go @@ -1,18 +1,21 @@ +//go:build linux + package storage import ( "os" "os/user" - "path/filepath" "strconv" drivers "github.com/containers/storage/drivers" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/unshare" "github.com/containers/storage/types" + securejoin "github.com/cyphar/filepath-securejoin" libcontainerUser "github.com/opencontainers/runc/libcontainer/user" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) // getAdditionalSubIDs looks up the additional IDs configured for @@ -78,43 +81,63 @@ func (s *store) getAvailableIDs() (*idSet, *idSet, error) { return u, g, nil } +const nobodyUser = 65534 // parseMountedFiles returns the maximum UID and GID found in the /etc/passwd and // /etc/group files. func parseMountedFiles(containerMount, passwdFile, groupFile string) uint32 { + var ( + passwd *os.File + group *os.File + size int + err error + ) if passwdFile == "" { - passwdFile = filepath.Join(containerMount, "etc/passwd") - } - if groupFile == "" { - groupFile = filepath.Join(groupFile, "etc/group") + passwd, err = secureOpen(containerMount, "/etc/passwd") + } else { + // User-specified override from a volume. Will not be in + // container root. + passwd, err = os.Open(passwdFile) } - - size := 0 - - users, err := libcontainerUser.ParsePasswdFile(passwdFile) if err == nil { - for _, u := range users { - // Skip the "nobody" user otherwise we end up with 65536 - // ids with most images - if u.Name == "nobody" { - continue - } - if u.Uid > size { - size = u.Uid - } - if u.Gid > size { - size = u.Gid + defer passwd.Close() + + users, err := libcontainerUser.ParsePasswd(passwd) + if err == nil { + for _, u := range users { + // Skip the "nobody" user otherwise we end up with 65536 + // ids with most images + if u.Name == "nobody" || u.Name == "nogroup" { + continue + } + if u.Uid > size && u.Uid != nobodyUser { + size = u.Uid + 1 + } + if u.Gid > size && u.Gid != nobodyUser { + size = u.Gid + 1 + } } } } - groups, err := libcontainerUser.ParseGroupFile(groupFile) + if groupFile == "" { + group, err = secureOpen(containerMount, "/etc/group") + } else { + // User-specified override from a volume. Will not be in + // container root. + group, err = os.Open(groupFile) + } if err == nil { - for _, g := range groups { - if g.Name == "nobody" { - continue - } - if g.Gid > size { - size = g.Gid + defer group.Close() + + groups, err := libcontainerUser.ParseGroup(group) + if err == nil { + for _, g := range groups { + if g.Name == "nobody" || g.Name == "nogroup" { + continue + } + if g.Gid > size && g.Gid != nobodyUser { + size = g.Gid + 1 + } } } } @@ -300,3 +323,19 @@ func getAutoUserNSIDMappings( gidMap := append(availableGIDs.zip(requestedContainerGIDs), additionalGIDMappings...) return uidMap, gidMap, nil } + +// Securely open (read-only) a file in a container mount. +func secureOpen(containerMount, file string) (*os.File, error) { + filePath, err := securejoin.SecureJoin(containerMount, file) + if err != nil { + return nil, err + } + + flags := unix.O_PATH | unix.O_CLOEXEC | unix.O_RDONLY + fileHandle, err := os.OpenFile(filePath, flags, 0) + if err != nil { + return nil, err + } + + return fileHandle, nil +} diff --git a/userns_unsupported.go b/userns_unsupported.go new file mode 100644 index 000000000..e37c18fe4 --- /dev/null +++ b/userns_unsupported.go @@ -0,0 +1,14 @@ +//go:build !linux + +package storage + +import ( + "errors" + + "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/types" +) + +func (s *store) getAutoUserNS(_ *types.AutoUserNsOptions, _ *Image, _ rwLayerStore, _ []roLayerStore) ([]idtools.IDMap, []idtools.IDMap, error) { + return nil, nil, errors.New("user namespaces are not supported on this platform") +} -- 2.25.1