From b92233f24f22bc509e2f9fad2dc67f49e482f363 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 CVE: CVE-2024-9676 Upstream-Status: Backport [0dc4fc9bb826e08b6e25af0af6a296ac172b5e15] 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 e0e530275..bea2a9520 100644 --- a/userns.go +++ b/userns.go @@ -1,18 +1,21 @@ +//go:build linux + package storage import ( "fmt" "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/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