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.

Addresses CVE-2024-9676

Signed-off-by: Matt Heon <mheon@redhat.com>
This commit is contained in:
Matt Heon 2024-10-09 09:54:22 -04:00
parent f53884cb5f
commit 491d72e9f2
3 changed files with 76 additions and 27 deletions

View File

@ -1,18 +1,21 @@
//go:build linux
package storage package storage
import ( import (
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
"path/filepath"
"strconv" "strconv"
drivers "github.com/containers/storage/drivers" drivers "github.com/containers/storage/drivers"
"github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/unshare" "github.com/containers/storage/pkg/unshare"
"github.com/containers/storage/types" "github.com/containers/storage/types"
securejoin "github.com/cyphar/filepath-securejoin"
libcontainerUser "github.com/moby/sys/user" libcontainerUser "github.com/moby/sys/user"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
) )
// getAdditionalSubIDs looks up the additional IDs configured for // getAdditionalSubIDs looks up the additional IDs configured for
@ -85,40 +88,59 @@ const nobodyUser = 65534
// parseMountedFiles returns the maximum UID and GID found in the /etc/passwd and // parseMountedFiles returns the maximum UID and GID found in the /etc/passwd and
// /etc/group files. // /etc/group files.
func parseMountedFiles(containerMount, passwdFile, groupFile string) uint32 { func parseMountedFiles(containerMount, passwdFile, groupFile string) uint32 {
var (
passwd *os.File
group *os.File
size int
err error
)
if passwdFile == "" { if passwdFile == "" {
passwdFile = filepath.Join(containerMount, "etc/passwd") passwd, err = secureOpen(containerMount, "/etc/passwd")
} else {
// User-specified override from a volume. Will not be in
// container root.
passwd, err = os.Open(passwdFile)
} }
if groupFile == "" {
groupFile = filepath.Join(containerMount, "etc/group")
}
size := 0
users, err := libcontainerUser.ParsePasswdFile(passwdFile)
if err == nil { if err == nil {
for _, u := range users { defer passwd.Close()
// Skip the "nobody" user otherwise we end up with 65536
// ids with most images users, err := libcontainerUser.ParsePasswd(passwd)
if u.Name == "nobody" || u.Name == "nogroup" { if err == nil {
continue for _, u := range users {
} // Skip the "nobody" user otherwise we end up with 65536
if u.Uid > size && u.Uid != nobodyUser { // ids with most images
size = u.Uid + 1 if u.Name == "nobody" || u.Name == "nogroup" {
} continue
if u.Gid > size && u.Gid != nobodyUser { }
size = u.Gid + 1 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 { if err == nil {
for _, g := range groups { defer group.Close()
if g.Name == "nobody" || g.Name == "nogroup" {
continue groups, err := libcontainerUser.ParseGroup(group)
} if err == nil {
if g.Gid > size && g.Gid != nobodyUser { for _, g := range groups {
size = g.Gid + 1 if g.Name == "nobody" || g.Name == "nogroup" {
continue
}
if g.Gid > size && g.Gid != nobodyUser {
size = g.Gid + 1
}
} }
} }
} }
@ -309,3 +331,14 @@ func getAutoUserNSIDMappings(
gidMap := append(availableGIDs.zip(requestedContainerGIDs), additionalGIDMappings...) gidMap := append(availableGIDs.zip(requestedContainerGIDs), additionalGIDMappings...)
return uidMap, gidMap, nil return uidMap, gidMap, nil
} }
// Securely open (read-only) a file in a container mount.
func secureOpen(containerMount, file string) (*os.File, error) {
tmpFile, err := securejoin.OpenInRoot(containerMount, file)
if err != nil {
return nil, err
}
defer tmpFile.Close()
return securejoin.Reopen(tmpFile, unix.O_RDONLY)
}

View File

@ -1,3 +1,5 @@
//go:build linux
package storage package storage
import ( import (

14
userns_unsupported.go Normal file
View File

@ -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")
}