Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rootfs: prohibit symlinks that conflicts with readonlyPaths and/or maskedPaths to prevent CVE-2023-27561 #3756

Closed

Conversation

AkihiroSuda
Copy link
Member

@AkihiroSuda AkihiroSuda commented Mar 6, 2023

EDIT: this PR was replaced by:


"/proc" and "/sys" in the container rootfs can no longer be symlinks, as they could be tricked to obtain the host root.

Fix #3751 ("CVE-2019-19921 re-introduction/regression")
Fix CVE-2023-27561

The CVE has been already public, so I'm just going to open this PR publicly.

@AkihiroSuda AkihiroSuda added the backport/1.1-todo A PR in main branch which needs to be backported to release-1.1 label Mar 6, 2023
@AkihiroSuda AkihiroSuda force-pushed the fix-CVE-2023-27561 branch 2 times, most recently from f3d075a to 7e7b082 Compare March 6, 2023 02:24
@AkihiroSuda AkihiroSuda requested a review from a team March 6, 2023 02:26
@AkihiroSuda
Copy link
Member Author

(We should also try to eliminate the TOCTOU issue, but just preventing the symlinks is probably enough as a quick fix for the CVE)

[[ "${output}" == *"must not be a symlink"* ]]
}

@test "mask paths [prohibit symlink /testdir2 (parent of /testdir2/testfile2)]" {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC: Should we prohibit these symlinks, or should we just prohibit symlinking /proc and /sys ?

The latter might be better for compatibility, but might be less secure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the former is the right approach (parents should not be symlinks).

Copy link
Contributor

@kolyshkin kolyshkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, left a couple of comments about how to simplify the code.

//
// For preventing CVE-2023-27561 (CVE-2019-19921 re-introduction/regression).
// https://github.com/opencontainers/runc/issues/3751
func validateRoot(config *configs.Config) error {
Copy link
Member

@cyphar cyphar Mar 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is still subject to races, I wonder if a better solution might be to make utils.WithProcfd take an argument (or have a utils.WithProcfdNoSymlinks variant) which blocks symlink traversal so that we don't allow mounting on symlinks for paths where we don't want indirection (such as for masked and read-only paths as well as procfs and sysfs)? With openat2 this can be done with RESOLVE_NO_SYMLINKS -- though I'll need to see what to do about path components... Doing it this way is a little bit closer to how libpathrs would be used so might make things easier for that transition too...

To be honest, I read the issue and I don't understand why this appears to be a race condition problem -- isn't the issue that the check we added for the original CVE is completely ignored now because utils.WithProcfd blindly resolves the symlink?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openat2

Too new (kernel 5.6)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the problem is just that securejoin follows symlinks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we have to continue to support following symlinks for most mount operations

# https://github.com/opencontainers/runc/issues/2683
@test "runc run [tmpfs mount with absolute symlink]" {
# in container, /conf -> /real/conf
mkdir -p rootfs/real/conf
ln -s /real/conf rootfs/conf
update_config ' .mounts += [{
type: "tmpfs",
source: "tmpfs",
destination: "/conf/stack",
options: ["ro", "nodev", "nosuid"]
}]
| .process.args |= ["true"]'
runc run test_busybox
[ "$status" -eq 0 ]
}

/proc and /sys can be exceptions though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we don't use WithProcfd for maskPath()

// maskPath masks the top of the specified path inside a container to avoid
// security issues from processes reading information from non-namespace aware
// mounts ( proc/kcore ).
// For files, maskPath bind mounts /dev/null over the top of the specified path.
// For directories, maskPath mounts read-only tmpfs over the top of the specified path.
func maskPath(path string, mountLabel string) error {
if err := mount("/dev/null", path, "", "", unix.MS_BIND, ""); err != nil && !errors.Is(err, os.ErrNotExist) {
if errors.Is(err, unix.ENOTDIR) {
return mount("tmpfs", path, "", "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel))
}
return err
}
return nil
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand how the issue got reintroduced then... I'm taking a look...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cyphar the issue was reintroduced because the checking code added by 3291d66:

if fi, err := os.Lstat(dest); err != nil {

depends on dest being the actual path provided by a user. It stopped working as it is supposed to after commit 0ca91f4#diff-e0932af06cb46065b2ef5fc89bc2bd9b880c9578e01b89ef4ecaeffb78a8bef0L331 changed from dest = filepath.Join (rootfs, dest) to dest = securejoin.SecureJoin(rootfs, dest).

Now, SecureJoin resolves all symlinks under dest and thus checking is now done for link destination not the link itself.

Copy link
Contributor

@kolyshkin kolyshkin Mar 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... thus the check "if dest is a symlink" is no longer working.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right. I kept missing than when I was looking over it. I personally prefer the approach in #3773. Doing a pre-flight "scan" of the rootfs seems less than ideal (neither approach protects against races but the pre-flight one is more obviously incapable of handling races -- the approach in #3773 seems closer to what libpathrs would do and actually fixes the core bug in the CVE fix I wrote).

"/proc" and "/sys" in the container rootfs can no longer be symlinks,
as they could be tricked to obtain the host root.

Fix issue 3751 ("CVE-2019-19921 re-introduction/regression")
Fix CVE-2023-27561

The CVE has been already public, so I'm just going to open this PR publicly.

Co-authored-by: Kir Kolyshkin <[email protected]>
Signed-off-by: Akihiro Suda <[email protected]>
@AkihiroSuda AkihiroSuda requested a review from kolyshkin March 14, 2023 00:05
@AkihiroSuda
Copy link
Member Author

@opencontainers/runc-maintainers What should we do with this?

Copy link
Contributor

@kolyshkin kolyshkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless @cyphar has any update on this, I think we should merge this (and backport to 1.1.5).

@kolyshkin
Copy link
Contributor

OTOH here's an alternative that makes the original fix from #2207 working again: #3773

@kolyshkin
Copy link
Contributor

Yet another alternative, just for the sake of completeness, is to make SecureJoin not follow symlinks. Here's what I ended up with:

  // noSymVFS is a VFS interface for securejoin which disallows symlinks in the unsafePath.
  type noSymVFS struct{}  
   
  func (o noSymVFS) Lstat(name string) (fi os.FileInfo, err error) {
          fi, err = os.Lstat(name)
          if err == nil && fi.Mode()&os.ModeSymlink == os.ModeSymlink {
                  return nil, fmt.Errorf("%q is a symlink", name)
          }
          return
  }
   
  func (o noSymVFS) Readlink(name string) (string, error) { return os.Readlink(name) }
   
  func secureJoinNoSymlinks(root, unsafePath string) (string, error) {
          return securejoin.SecureJoinVFS(root, unsafePath, &noSymVFS{})
  }
   
  func mountToRootfs(m *configs.Mount, c *mountConfig) error {
  ...          
          secJoin := securejoin.SecureJoin
          if m.Device == "proc" || m.Device == "sysfs" {
                  // If the destination already exists and is not a directory, we bail
                  // out. This is to avoid mounting through a symlink or similar -- which
                  // has been a "fun" attack scenario in the past.
                  // TODO: This won't be necessary once we switch to libpathrs and we can
                  //       stop all of these symlink-exchange attacks.
                  secJoin = secureJoinNoSymlinks
          }
          dest, err := secJoin(rootfs, m.Destination)
          

This has not been tested but I definitely don't like this code. The approach used in #3773 (or here) seems better.

@AkihiroSuda
Copy link
Member Author

@kolyshkin kolyshkin removed the backport/1.1-todo A PR in main branch which needs to be backported to release-1.1 label Apr 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CVE-2019-19921 re-introduction/regression
4 participants