Giter VIP home page Giter VIP logo

filepath-securejoin's Introduction

filepath-securejoin

Build Status

Old API

This library was originally just an implementation of SecureJoin which was intended to be included in the Go standard library as a safer filepath.Join that would restrict the path lookup to be inside a root directory.

The implementation was based on code that existed in several container runtimes. Unfortunately, this API is fundamentally unsafe against attackers that can modify path components after SecureJoin returns and before the caller uses the path, allowing for some fairly trivial TOCTOU attacks.

SecureJoin (and SecureJoinVFS) are still provided by this library to support legacy users, but new users are strongly suggested to avoid using SecureJoin and instead use the new api or switch to libpathrs.

With the above limitations in mind, this library guarantees the following:

  • If no error is set, the resulting string must be a child path of root and will not contain any symlink path components (they will all be expanded).

  • When expanding symlinks, all symlink path components must be resolved relative to the provided root. In particular, this can be considered a userspace implementation of how chroot(2) operates on file paths. Note that these symlinks will not be expanded lexically (filepath.Clean is not called on the input before processing).

  • Non-existent path components are unaffected by SecureJoin (similar to filepath.EvalSymlinks's semantics).

  • The returned path will always be filepath.Cleaned and thus not contain any .. components.

A (trivial) implementation of this function on GNU/Linux systems could be done with the following (note that this requires root privileges and is far more opaque than the implementation in this library, and also requires that readlink is inside the root path and is trustworthy):

package securejoin

import (
	"os/exec"
	"path/filepath"
)

func SecureJoin(root, unsafePath string) (string, error) {
	unsafePath = string(filepath.Separator) + unsafePath
	cmd := exec.Command("chroot", root,
		"readlink", "--canonicalize-missing", "--no-newline", unsafePath)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", err
	}
	expanded := string(output)
	return filepath.Join(root, expanded), nil
}

New API

While we recommend users switch to libpathrs as soon as it has a stable release, some methods implemented by libpathrs have been ported to this library to ease the transition. These APIs are only supported on Linux.

These APIs are implemented such that filepath-securejoin will opportunistically use certain newer kernel APIs that make these operations far more secure. In particular:

  • All of the lookup operations will use openat2 on new enough kernels (Linux 5.6 or later) to restrict lookups through magic-links and bind-mounts (for certain operations) and to make use of RESOLVE_IN_ROOT to efficiently resolve symlinks within a rootfs.

  • The APIs provide hardening against a malicious /proc mount to either detect or avoid being tricked by a /proc that is not legitimate. This is done using openat2 for all users, and privileged users will also be further protected by using fsopen and open_tree (Linux 4.18 or later).

OpenInRoot

func OpenInRoot(root, unsafePath string) (*os.File, error)
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
func Reopen(handle *os.File, flags int) (*os.File, error)

OpenInRoot is a much safer version of

path, err := securejoin.SecureJoin(root, unsafePath)
file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)

that protects against various race attacks that could lead to serious security issues, depending on the application. Note that the returned *os.File is an O_PATH file descriptor, which is quite restricted. Callers will probably need to use Reopen to get a more usable handle (this split is done to provide useful features like PTY spawning and to avoid users accidentally opening bad inodes that could cause a DoS).

Callers need to be careful in how they use the returned *os.File. Usually it is only safe to operate on the handle directly, and it is very easy to create a security issue. libpathrs provides far more helpers to make using these handles safer -- there is currently no plan to port them to filepath-securejoin.

OpenatInRoot is like OpenInRoot except that the root is provided using an *os.File. This allows you to ensure that multiple OpenatInRoot (or MkdirAllHandle) calls are operating on the same rootfs.

NOTE: Unlike SecureJoin, OpenInRoot will error out as soon as it hits a dangling symlink or non-existent path. This is in contrast to SecureJoin which treated non-existent components as though they were real directories, and would allow for partial resolution of dangling symlinks. These behaviours are at odds with how Linux treats non-existent paths and dangling symlinks, and so these are no longer allowed.

MkdirAll

func MkdirAll(root, unsafePath string, mode int) error
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)

MkdirAll is a much safer version of

path, err := securejoin.SecureJoin(root, unsafePath)
err = os.MkdirAll(path, mode)

that protects against the same kinds of races that OpenInRoot protects against.

MkdirAllHandle is like MkdirAll except that the root is provided using an *os.File (the reason for this is the same as with OpenatInRoot) and an *os.File of the final created directory is returned (this directory is guaranteed to be effectively identical to the directory created by MkdirAllHandle, which is not possible to ensure by just using OpenatInRoot after MkdirAll).

NOTE: Unlike SecureJoin, MkdirAll will error out as soon as it hits a dangling symlink or non-existent path. This is in contrast to SecureJoin which treated non-existent components as though they were real directories, and would allow for partial resolution of dangling symlinks. These behaviours are at odds with how Linux treats non-existent paths and dangling symlinks, and so these are no longer allowed. This means that MkdirAll will not create non-existent directories referenced by a dangling symlink.

License

The license of this project is the same as Go, which is a BSD 3-clause license available in the LICENSE file.

filepath-securejoin's People

Contributors

cyphar avatar dependabot[bot] avatar dthadi3 avatar jwilk avatar kolyshkin avatar pjbgf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

filepath-securejoin's Issues

consider migrating to io/fs.StatFS for SecureJoinVFS

Now the Go has their own "generic" interfaces for filesystem roots, maybe it would make sense to migrate to using that rather than our own home-brew thing. Note that we want to be given the equivalent of fs.DirFS("/") not fs.DirFS(root) -- we are implementing our own resolution logic, and the unsafe path.Join logic in io/fs is not going to work.

loop error message contains a non-existent path

The error message that is triggered by too many symlink dereferences contains a non-existent path instead of the path to the symlink that causes the loop.

The path is set here:

return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}

Steps to reproduce the issue

On Fedora CoreOS perform the following steps

  1. mkdir ~/test
  2. cd ~/test
  3. create the file Dockerfile with this contents
    FROM docker.io/library/fedora:38
    RUN dnf install -y golang
    
  4. podman build -t go .
  5. create the file reproducer.go with this contents
    package main
    import (
        "os"
        "errors"
        "fmt"
        securejoin "github.com/cyphar/filepath-securejoin"
    )
    func main() {
      path := "/root/sub"
      os.MkdirAll(path, 0755)
      target := "symlink"
      symlink := "/root/sub/symlink"
      os.Symlink(target, symlink)
      _, err := securejoin.SecureJoin("/root","sub/symlink")
      if (err != nil) {
        var pErr *os.PathError
        if errors.As(err, &pErr) {
          fmt.Printf("Error = %v\n", pErr.Err)
          fmt.Printf("Path = %v\n", pErr.Path)
        }
      }
    }
  6. podman run --rm -v ./:/src:Z -w /src localhost/go go mod init reproducer
  7. podman run --rm -v ./:/src:Z -w /src localhost/go go mod tidy
  8. podman run --rm -v ./:/src:Z -w /src localhost/go go run .
    The command prints
    go: downloading github.com/cyphar/filepath-securejoin v0.2.4
    Error = too many levels of symbolic links
    Path = /root/symlink/
    

Describe the results you received

At step 8 I see

Path = /root/symlink/

Describe the results you expected

At step 8 I would have expected to see

Path = /root/sub/symlink

as /root/sub/symlink is the path to the symlink that points to itself. The path /root/symlink/ does not exist.

Comment

I'm not blocked by this issue in any way. Probably this issue is of low importance.

proc: should we bother with some minor improvements?

We could improve the safety of our /proc/self/fd handling a little by:

  1. Using readlinkat(fd, "") for readlinks, which should will eliminate bind-mount attacks against readlink (for unsafe /proc handles).
  2. For pre-5.8 kernels we can (ab)use name_to_handle_at to get the mount ID of a file descriptor directly. The mount ID we get is not a unique mount ID unfortunately, but it is just as good as what we get from pre-6.11 kernels.

libpathrs will use these, but I'm not sure whether it's worth porting these marginal security hardenings to filepath-securejoin...

OpenInRoot: O_NOFOLLOW support?

It would be fairly trivial to implement this, and I suspect some users will need it. I'm not sure if it makes sense to add to this project though, given that we want to push users to libpathrs (which will have this feature).

(Basically this would be like O_NOFOLLOW.)

Error not being raised when expected for unsafePath of `../upperfile.txt`

This might be more of a question than an issue, but I'll log it anyway and see what the expectation is.

In https://github.com/cyphar/filepath-securejoin/blob/master/join.go#L61, if the values passed in to SecureJoinVFS are:

(/root/testdir, ../upperfile.txt, nil) and on the machine the file /root/upperfile.txt exists, but the file /root/testdir/upperfile.txt does not, SecureJoinVFS() is not returning an error as I would expect.

Instead, it's cleaning up the .. and returning (/root/testdir/upperfile.txt, nil)

If by chance I had a /root/testdir/upperfile.txt I still would not expect to get a pointer to it instead of /root/upperfile.txt.

I think the correct behavior would be to return an error in this circumstance. Thoughts?

FWIW, this came up due to an investigation into a Podman issue logged here: containers/podman#5035

cut a release

Can we have a tagged release? Mostly interested in having #7 included into a released version.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.