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

Prohibit /proc and /sys to be symlinks #3773

Merged
merged 1 commit into from
Mar 25, 2023

Conversation

kolyshkin
Copy link
Contributor

@kolyshkin kolyshkin commented Mar 16, 2023

Commit 3291d66 introduced a check for /proc and /sys, making sure the destination (dest) is a directory (and not e.g. a symlink).

Later, a hunk from commit 0ca91f4 switched from using filepath.Join to SecureJoin for dest. As SecureJoin follows and resolves symlinks, the check whether dest is a symlink no longer works.

To fix, do the check without/before using SecureJoin.

Add integration tests (stolen from #3756) to make sure we won't regress.

This is an alternative to #3756.

Fixes: #3751
Fixes: CVE-2023-27561

@cyphar
Copy link
Member

cyphar commented Mar 17, 2023

With the above comment in mind, I prefer this patch to #3756.

// 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.
dest := m.Destination
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to filepath.Clean() before checking the prefix?

Copy link
Member

@cyphar cyphar Mar 17, 2023

Choose a reason for hiding this comment

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

dest := filepath.Clean("/"+m.Destination) probably makes the most sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the light of that, I guess, we need to finish what we started in commits 1f1e91b and b31a934. Before doing that, I guess, we could not use filepath.Clean("/"+m.Destination).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And once we'll finish it, it won't be necessary to prepend "/". So, I guess, bare filepath.Clean is enough.

@kolyshkin kolyshkin force-pushed the no-symlinks branch 2 times, most recently from 33124cc to 72eeffd Compare March 17, 2023 16:50
@kolyshkin kolyshkin marked this pull request as ready for review March 17, 2023 16:50
@kolyshkin
Copy link
Contributor Author

OK, so I have to add this:

--- a/tests/integration/mask.bats
+++ b/tests/integration/mask.bats
@@ -65,6 +65,9 @@ function teardown() {
 }
 
 @test "mask paths [prohibit symlink /sys]" {
+       # In rootless containers, /sys is a bind mount not a real sysfs.
+       requires root
+
        ln -s /symlink rootfs/sys
        runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
        [ "$status" -eq 1 ]

because in rootless containers /sys is not a sysfs mount but a bind mount, so the sysfs case is not working.

I guess it's not a problem for rootless containers anyway, so relaxing the check is fine.

Commit 3291d66 introduced a check for /proc and /sys, making sure
the destination (dest) is a directory (and not e.g. a symlink).

Later, a hunk from commit 0ca91f4 switched from using filepath.Join
to SecureJoin for dest. As SecureJoin follows and resolves symlinks,
the check whether dest is a symlink no longer works.

To fix, do the check without/before using SecureJoin.

Add integration tests to make sure we won't regress.

Signed-off-by: Kir Kolyshkin <[email protected]>
@kolyshkin
Copy link
Contributor Author

Thinking about one more alternative, which is:

  1. ensure dest is cleaned
  2. open an fd at roofs+dest with O_PATH|O_NOFOLLOW|O_DIRECTORY
  3. In case of ENOENT, mkdir and retry once; bail out for other errors.
  4. pass the fd to mountPropagate

This way we'll remove the TOCTOU race and we don't even need openat2.

WDYT @cyphar ?

@kolyshkin kolyshkin requested review from thaJeztah and cyphar March 17, 2023 19:13
@kolyshkin
Copy link
Contributor Author

Thinking about one more alternative, which is:

  1. ensure dest is cleaned
  2. open an fd at roofs+dest with O_PATH|O_NOFOLLOW|O_DIRECTORY

This might cause rootfs escape if any parent components of dest are symlinks.

So, maybe, do readlink on the file descriptor and check that rootfs is its prefix,
and bail out otherwise.

  1. In case of ENOENT, mkdir and retry once; bail out for other errors.
  2. pass the fd to mountPropagate

This way we'll remove the TOCTOU race and we don't even need openat2.

WDYT @cyphar ?

@cyphar
Copy link
Member

cyphar commented Mar 24, 2023

@kolyshkin I don't really like that idea. I would prefer if we just use this solution now and we can solve this (and the many other issues we have) with libpathrs in the future.

You can have container configs that mount sysfs and procfs in places other than /sys and /proc and I'd be far too concerned with that resulting in another awful symlink-related container attack.

Copy link
Member

@cyphar cyphar left a comment

Choose a reason for hiding this comment

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

LGTM.

@thaJeztah
Copy link
Member

Probably not for this PR, but I noticed that we're inconsistent in handling "where to return". It looks like all switch cases have a return, except for one, and that one is put outside of the switch;

if err := setRecAttr(m, rootfs); err != nil {
return err
}
return nil

Perhaps we should move that one inside the case "bind" branch to make it clearer what condition it relates to. Doing so may also help linters detect that we're missing a return statement if we would add a new branch to any of the switches.

case "bind":

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

LGTM

@AkihiroSuda PTAL

@AkihiroSuda AkihiroSuda merged commit 948ef27 into opencontainers:main Mar 25, 2023
@thaJeztah
Copy link
Member

I guess this one needs to be cherry-picked into the release-1.1 branch?

@thaJeztah thaJeztah added the backport/1.1-todo A PR in main branch which needs to be backported to release-1.1 label Mar 25, 2023
@thaJeztah
Copy link
Member

opened #3785

@thaJeztah thaJeztah added this to the 1.2.0 milestone Mar 25, 2023
@thaJeztah thaJeztah added backport/1.1-done A PR in main branch which has been backported to release-1.1 and removed backport/1.1-todo A PR in main branch which needs to be backported to release-1.1 labels Mar 26, 2023
@kolyshkin
Copy link
Contributor Author

Probably not for this PR, but I noticed that we're inconsistent in handling "where to return". It looks like all switch cases have a return, except for one, and that one is put outside of the switch;

if err := setRecAttr(m, rootfs); err != nil {
return err
}
return nil

Perhaps we should move that one inside the case "bind" branch to make it clearer what condition it relates to. Doing so may also help linters detect that we're missing a return statement if we would add a new branch to any of the switches.

case "bind":

Makes sense, opened #3787

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport/1.1-done A PR in main branch which has been backported to release-1.1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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