kube play: don't follow volume symlinks onto the host

For ConfigMap and Secret kube play volumes podman populates the data
from the yaml. However the volume content is not controlled by us and we
can be tricked following a symlink to a file on the host instead.

Fixes: CVE-2025-9566

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger
2025-08-29 15:39:38 +02:00
parent 2499de2f27
commit 43fbde4e66
4 changed files with 71 additions and 4 deletions

View File

@@ -810,8 +810,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
defaultMode := v.DefaultMode
// Create files and add data to the volume mountpoint based on the Items in the volume
for k, v := range v.Items {
dataPath := filepath.Join(mountPoint, k)
f, err := os.Create(dataPath)
f, err := openPathSafely(mountPoint, k)
if err != nil {
return nil, nil, fmt.Errorf("cannot create file %q at volume mountpoint %q: %w", k, mountPoint, err)
}
@@ -821,7 +820,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
return nil, nil, err
}
// Set file permissions
if err := os.Chmod(f.Name(), os.FileMode(defaultMode)); err != nil {
if err := f.Chmod(os.FileMode(defaultMode)); err != nil {
return nil, nil, err
}
}

View File

@@ -0,0 +1,18 @@
//go:build !remote
package abi
import (
"os"
securejoin "github.com/cyphar/filepath-securejoin"
)
// openSymlinkPath opens the path under root using securejoin.OpenatInRoot().
func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
file, err := securejoin.OpenatInRoot(root, unsafePath)
if err != nil {
return nil, err
}
return securejoin.Reopen(file, flags)
}

View File

@@ -0,0 +1,13 @@
//go:build !linux && !remote
package abi
import (
"errors"
"os"
)
// openSymlinkPath is not supported on this platform.
func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
return nil, errors.New("cannot safely open symlink on this platform")
}

View File

@@ -2,7 +2,14 @@
package abi
import "github.com/containers/podman/v5/libpod/define"
import (
"fmt"
"os"
"strings"
"github.com/containers/podman/v5/libpod/define"
"golang.org/x/sys/unix"
)
// getSdNotifyMode returns the `sdNotifyAnnotation/$name` for the specified
// name. If name is empty, it'll only look for `sdNotifyAnnotation`.
@@ -16,3 +23,33 @@ func getSdNotifyMode(annotations map[string]string, name string) (string, error)
}
return mode, define.ValidateSdNotifyMode(mode)
}
// openPathSafely opens the given name under the trusted root path, the unsafeName
// must be a single path component and not contain "/".
// The resulting path will be opened or created if it does not exists.
// Following of symlink is done within staying under root, escapes outsides
// of root are not allowed and prevent.
//
// This custom function is needed because securejoin.SecureJoin() is not race safe
// and the volume might be mounted in another container that could swap in a symlink
// after the function ahs run. securejoin.OpenInRoot() doesn't work either because
// it cannot create files and doesn't work on freebsd.
func openPathSafely(root, unsafeName string) (*os.File, error) {
if strings.Contains(unsafeName, "/") {
return nil, fmt.Errorf("name %q must not contain path separator", unsafeName)
}
fdDir, err := os.OpenFile(root, unix.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer fdDir.Close()
flags := unix.O_CREAT | unix.O_WRONLY | unix.O_TRUNC | unix.O_CLOEXEC
fd, err := unix.Openat(int(fdDir.Fd()), unsafeName, flags|unix.O_NOFOLLOW, 0o644)
if err == nil {
return os.NewFile(uintptr(fd), unsafeName), nil
}
if err == unix.ELOOP {
return openSymlinkPath(fdDir, unsafeName, flags)
}
return nil, &os.PathError{Op: "openat", Path: unsafeName, Err: err}
}