pax_global_header00006660000000000000000000000064140703142600014507gustar00rootroot0000000000000052 comment=8f267f5ea675a20a2cb5e91011d063721f53bf79 filepath-securejoin-0.2.3/000077500000000000000000000000001407031426000154515ustar00rootroot00000000000000filepath-securejoin-0.2.3/.travis.yml000066400000000000000000000005221407031426000175610ustar00rootroot00000000000000# Copyright (C) 2017 SUSE LLC. All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. language: go go: - 1.13.x - 1.16.x - tip arch: - AMD64 - ppc64le os: - linux - osx script: - go test -cover -v ./... notifications: email: false filepath-securejoin-0.2.3/LICENSE000066400000000000000000000030071407031426000164560ustar00rootroot00000000000000Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. Copyright (C) 2017 SUSE LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. filepath-securejoin-0.2.3/README.md000066400000000000000000000057351407031426000167420ustar00rootroot00000000000000## `filepath-securejoin` ## [![Build Status](https://travis-ci.org/cyphar/filepath-securejoin.svg?branch=master)](https://travis-ci.org/cyphar/filepath-securejoin) An implementation of `SecureJoin`, a [candidate for inclusion in the Go standard library][go#20126]. The purpose of this function is to be a "secure" alternative to `filepath.Join`, and in particular it provides certain guarantees that are not provided by `filepath.Join`. > **NOTE**: This code is *only* safe if you are not at risk of other processes > modifying path components after you've used `SecureJoin`. If it is possible > for a malicious process to modify path components of the resolved path, then > you will be vulnerable to some fairly trivial TOCTOU race conditions. [There > are some Linux kernel patches I'm working on which might allow for a better > solution.][lwn-obeneath] > > In addition, with a slightly modified API it might be possible to use > `O_PATH` and verify that the opened path is actually the resolved one -- but > I have not done that yet. I might add it in the future as a helper function > to help users verify the path (we can't just return `/proc/self/fd/` > because that doesn't always work transparently for all users). This is the function prototype: ```go func SecureJoin(root, unsafePath string) (string, error) ``` 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.Clean`ed 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): ```go 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 } ``` [lwn-obeneath]: https://lwn.net/Articles/767547/ [go#20126]: https://github.com/golang/go/issues/20126 ### 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-0.2.3/VERSION000066400000000000000000000000061407031426000165150ustar00rootroot000000000000000.2.3 filepath-securejoin-0.2.3/go.mod000066400000000000000000000000661407031426000165610ustar00rootroot00000000000000module github.com/cyphar/filepath-securejoin go 1.13 filepath-securejoin-0.2.3/join.go000066400000000000000000000102011407031426000167310ustar00rootroot00000000000000// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. // Copyright (C) 2017 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package securejoin is an implementation of the hopefully-soon-to-be-included // SecureJoin helper that is meant to be part of the "path/filepath" package. // The purpose of this project is to provide a PoC implementation to make the // SecureJoin proposal (https://github.com/golang/go/issues/20126) more // tangible. package securejoin import ( "bytes" "errors" "os" "path/filepath" "strings" "syscall" ) // IsNotExist tells you if err is an error that implies that either the path // accessed does not exist (or path components don't exist). This is // effectively a more broad version of os.IsNotExist. func IsNotExist(err error) bool { // Check that it's not actually an ENOTDIR, which in some cases is a more // convoluted case of ENOENT (usually involving weird paths). return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT) } // SecureJoinVFS joins the two given path components (similar to Join) except // that the returned path is guaranteed to be scoped inside the provided root // path (when evaluated). Any symbolic links in the path are evaluated with the // given root treated as the root of the filesystem, similar to a chroot. The // filesystem state is evaluated through the given VFS interface (if nil, the // standard os.* family of functions are used). // // Note that the guarantees provided by this function only apply if the path // components in the returned string are not modified (in other words are not // replaced with symlinks on the filesystem) after this function has returned. // Such a symlink race is necessarily out-of-scope of SecureJoin. func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { // Use the os.* VFS implementation if none was specified. if vfs == nil { vfs = osVFS{} } var path bytes.Buffer n := 0 for unsafePath != "" { if n > 255 { return "", &os.PathError{Op: "SecureJoin", Path: root + "/" + unsafePath, Err: syscall.ELOOP} } // Next path component, p. i := strings.IndexRune(unsafePath, filepath.Separator) var p string if i == -1 { p, unsafePath = unsafePath, "" } else { p, unsafePath = unsafePath[:i], unsafePath[i+1:] } // Create a cleaned path, using the lexical semantics of /../a, to // create a "scoped" path component which can safely be joined to fullP // for evaluation. At this point, path.String() doesn't contain any // symlink components. cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p) if cleanP == string(filepath.Separator) { path.Reset() continue } fullP := filepath.Clean(root + cleanP) // Figure out whether the path is a symlink. fi, err := vfs.Lstat(fullP) if err != nil && !IsNotExist(err) { return "", err } // Treat non-existent path components the same as non-symlinks (we // can't do any better here). if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 { path.WriteString(p) path.WriteRune(filepath.Separator) continue } // Only increment when we actually dereference a link. n++ // It's a symlink, expand it by prepending it to the yet-unparsed path. dest, err := vfs.Readlink(fullP) if err != nil { return "", err } // Absolute symlinks reset any work we've already done. if filepath.IsAbs(dest) { path.Reset() } unsafePath = dest + string(filepath.Separator) + unsafePath } // We have to clean path.String() here because it may contain '..' // components that are entirely lexical, but would be misleading otherwise. // And finally do a final clean to ensure that root is also lexically // clean. fullP := filepath.Clean(string(filepath.Separator) + path.String()) return filepath.Clean(root + fullP), nil } // SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library // of functions as the VFS. If in doubt, use this function over SecureJoinVFS. func SecureJoin(root, unsafePath string) (string, error) { return SecureJoinVFS(root, unsafePath, nil) } filepath-securejoin-0.2.3/join_test.go000066400000000000000000000302371407031426000200030ustar00rootroot00000000000000// Copyright (C) 2017 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package securejoin import ( "errors" "io/ioutil" "os" "path/filepath" "syscall" "testing" ) // TODO: These tests won't work on plan9 because it doesn't have symlinks, and // also we use '/' here explicitly which probably won't work on Windows. func symlink(t *testing.T, oldname, newname string) { if err := os.Symlink(oldname, newname); err != nil { t.Fatal(err) } } // Test basic handling of symlink expansion. func TestSymlink(t *testing.T) { dir, err := ioutil.TempDir("", "TestSymlink") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) symlink(t, "somepath", filepath.Join(dir, "etc")) symlink(t, "../../../../../../../../../../../../../etc", filepath.Join(dir, "etclink")) symlink(t, "/../../../../../../../../../../../../../etc/passwd", filepath.Join(dir, "passwd")) for _, test := range []struct { root, unsafe string expected string }{ // Make sure that expansion with a root of '/' proceeds in the expected fashion. {"/", filepath.Join(dir, "passwd"), "/etc/passwd"}, {"/", filepath.Join(dir, "etclink"), "/etc"}, {"/", filepath.Join(dir, "etc"), filepath.Join(dir, "somepath")}, // Now test scoped expansion. {dir, "passwd", filepath.Join(dir, "somepath", "passwd")}, {dir, "etclink", filepath.Join(dir, "somepath")}, {dir, "etc", filepath.Join(dir, "somepath")}, {dir, "etc/test", filepath.Join(dir, "somepath", "test")}, {dir, "etc/test/..", filepath.Join(dir, "somepath")}, } { got, err := SecureJoin(test.root, test.unsafe) if err != nil { t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err) continue } // This is only for OS X, where /etc is a symlink to /private/etc. In // principle, SecureJoin(/, pth) is the same as EvalSymlinks(pth) in // the case where the path exists. if test.root == "/" { if expected, err := filepath.EvalSymlinks(test.expected); err == nil { test.expected = expected } } if got != test.expected { t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got) continue } } } // In a path without symlinks, SecureJoin is equivalent to Clean+Join. func TestNoSymlink(t *testing.T) { dir, err := ioutil.TempDir("", "TestNoSymlink") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) for _, test := range []struct { root, unsafe string }{ // TODO: Do we need to have some conditional FromSlash handling here? {dir, "somepath"}, {dir, "even/more/path"}, {dir, "/this/is/a/path"}, {dir, "also/a/../path/././/with/some/./.././junk"}, {dir, "yetanother/../path/././/with/some/./.././junk../../../../../../../../../../../../etc/passwd"}, {dir, "/../../../../../../../../../../../../../../../../etc/passwd"}, {dir, "../../../../../../../../../../../../../../../../somedir"}, {dir, "../../../../../../../../../../../../../../../../"}, {dir, "./../../.././././../../../../../../../../../../../../../../../../etc passwd"}, } { expected := filepath.Join(test.root, filepath.Clean(string(filepath.Separator)+test.unsafe)) got, err := SecureJoin(test.root, test.unsafe) if err != nil { t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err) continue } if got != expected { t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, expected, got) continue } } } // Make sure that .. is **not** expanded lexically. func TestNonLexical(t *testing.T) { dir, err := ioutil.TempDir("", "TestNonLexical") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) os.MkdirAll(filepath.Join(dir, "subdir"), 0755) os.MkdirAll(filepath.Join(dir, "cousinparent", "cousin"), 0755) symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link")) symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2")) symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3")) for _, test := range []struct { root, unsafe string expected string }{ {dir, "subdir", filepath.Join(dir, "subdir")}, {dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/../test", filepath.Join(dir, "test")}, // This is the divergence from a simple filepath.Clean implementation. {dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")}, {dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")}, {dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")}, } { got, err := SecureJoin(test.root, test.unsafe) if err != nil { t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err) continue } if got != test.expected { t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got) continue } } } // Make sure that symlink loops result in errors. func TestSymlinkLoop(t *testing.T) { dir, err := ioutil.TempDir("", "TestSymlinkLoop") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) os.MkdirAll(filepath.Join(dir, "subdir"), 0755) symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "subdir", "link")) symlink(t, "/subdir/link", filepath.Join(dir, "path")) symlink(t, "/../../../../../../../../../../../../../../../../self", filepath.Join(dir, "self")) for _, test := range []struct { root, unsafe string }{ {dir, "subdir/link"}, {dir, "path"}, {dir, "../../path"}, {dir, "subdir/link/../.."}, {dir, "../../../../../../../../../../../../../../../../subdir/link/../../../../../../../../../../../../../../../.."}, {dir, "self"}, {dir, "self/.."}, {dir, "/../../../../../../../../../../../../../../../../self/.."}, {dir, "/self/././.."}, } { got, err := SecureJoin(test.root, test.unsafe) if !errors.Is(err, syscall.ELOOP) { t.Errorf("securejoin(%q, %q): expected ELOOP, got %v & %q", test.root, test.unsafe, err, got) continue } } } // Make sure that ENOTDIR is correctly handled. func TestEnotdir(t *testing.T) { dir, err := ioutil.TempDir("", "TestEnotdir") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) os.MkdirAll(filepath.Join(dir, "subdir"), 0755) ioutil.WriteFile(filepath.Join(dir, "notdir"), []byte("I am not a directory!"), 0755) symlink(t, "/../../../notdir/somechild", filepath.Join(dir, "subdir", "link")) for _, test := range []struct { root, unsafe string }{ {dir, "subdir/link"}, {dir, "notdir"}, {dir, "notdir/child"}, } { _, err := SecureJoin(test.root, test.unsafe) if err != nil { t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err) continue } } } // Some silly tests to make sure that all error types are correctly handled. func TestIsNotExist(t *testing.T) { for _, test := range []struct { err error expected bool }{ {&os.PathError{Op: "test1", Err: syscall.ENOENT}, true}, {&os.LinkError{Op: "test1", Err: syscall.ENOENT}, true}, {&os.SyscallError{Syscall: "test1", Err: syscall.ENOENT}, true}, {&os.PathError{Op: "test2", Err: syscall.ENOTDIR}, true}, {&os.LinkError{Op: "test2", Err: syscall.ENOTDIR}, true}, {&os.SyscallError{Syscall: "test2", Err: syscall.ENOTDIR}, true}, {&os.PathError{Op: "test3", Err: syscall.EACCES}, false}, {&os.LinkError{Op: "test3", Err: syscall.EACCES}, false}, {&os.SyscallError{Syscall: "test3", Err: syscall.EACCES}, false}, {errors.New("not a proper error"), false}, } { got := IsNotExist(test.err) if got != test.expected { t.Errorf("IsNotExist(%#v): expected %v, got %v", test.err, test.expected, got) } } } type mockVFS struct { lstat func(path string) (os.FileInfo, error) readlink func(path string) (string, error) } func (m mockVFS) Lstat(path string) (os.FileInfo, error) { return m.lstat(path) } func (m mockVFS) Readlink(path string) (string, error) { return m.readlink(path) } // Make sure that SecureJoinVFS actually does use the given VFS interface. func TestSecureJoinVFS(t *testing.T) { dir, err := ioutil.TempDir("", "TestNonLexical") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) os.MkdirAll(filepath.Join(dir, "subdir"), 0755) os.MkdirAll(filepath.Join(dir, "cousinparent", "cousin"), 0755) symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link")) symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2")) symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3")) for _, test := range []struct { root, unsafe string expected string }{ {dir, "subdir", filepath.Join(dir, "subdir")}, {dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")}, {dir, "subdir/../test", filepath.Join(dir, "test")}, // This is the divergence from a simple filepath.Clean implementation. {dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")}, {dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")}, {dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")}, } { var nLstat, nReadlink int mock := mockVFS{ lstat: func(path string) (os.FileInfo, error) { nLstat++; return os.Lstat(path) }, readlink: func(path string) (string, error) { nReadlink++; return os.Readlink(path) }, } got, err := SecureJoinVFS(test.root, test.unsafe, mock) if err != nil { t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err) continue } if got != test.expected { t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got) continue } if nLstat == 0 && nReadlink == 0 { t.Errorf("securejoin(%q, %q): expected to use either lstat or readlink, neither were used", test.root, test.unsafe) } } } // Make sure that SecureJoinVFS actually does use the given VFS interface, and // that errors are correctly propagated. func TestSecureJoinVFSErrors(t *testing.T) { var ( lstatErr = errors.New("lstat error") readlinkErr = errors.New("readlink err") ) // Set up directory. dir, err := ioutil.TempDir("", "TestSecureJoinVFSErrors") if err != nil { t.Fatal(err) } dir, err = filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) // Make a link. symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "link")) // Define some fake mock functions. lstatFailFn := func(path string) (os.FileInfo, error) { return nil, lstatErr } readlinkFailFn := func(path string) (string, error) { return "", readlinkErr } // Make sure that the set of {lstat, readlink} failures do propagate. for idx, test := range []struct { vfs VFS expected []error }{ { expected: []error{nil}, vfs: mockVFS{ lstat: os.Lstat, readlink: os.Readlink, }, }, { expected: []error{lstatErr}, vfs: mockVFS{ lstat: lstatFailFn, readlink: os.Readlink, }, }, { expected: []error{readlinkErr}, vfs: mockVFS{ lstat: os.Lstat, readlink: readlinkFailFn, }, }, { expected: []error{lstatErr, readlinkErr}, vfs: mockVFS{ lstat: lstatFailFn, readlink: readlinkFailFn, }, }, } { _, err := SecureJoinVFS(dir, "link", test.vfs) success := false for _, exp := range test.expected { if err == exp { success = true } } if !success { t.Errorf("SecureJoinVFS.mock%d: expected to get lstatError, got %v", idx, err) } } } filepath-securejoin-0.2.3/vfs.go000066400000000000000000000032501407031426000165760ustar00rootroot00000000000000// Copyright (C) 2017 SUSE LLC. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package securejoin import "os" // In future this should be moved into a separate package, because now there // are several projects (umoci and go-mtree) that are using this sort of // interface. // VFS is the minimal interface necessary to use SecureJoinVFS. A nil VFS is // equivalent to using the standard os.* family of functions. This is mainly // used for the purposes of mock testing, but also can be used to otherwise use // SecureJoin with VFS-like system. type VFS interface { // Lstat returns a FileInfo describing the named file. If the file is a // symbolic link, the returned FileInfo describes the symbolic link. Lstat // makes no attempt to follow the link. These semantics are identical to // os.Lstat. Lstat(name string) (os.FileInfo, error) // Readlink returns the destination of the named symbolic link. These // semantics are identical to os.Readlink. Readlink(name string) (string, error) } // osVFS is the "nil" VFS, in that it just passes everything through to the os // module. type osVFS struct{} // Lstat returns a FileInfo describing the named file. If the file is a // symbolic link, the returned FileInfo describes the symbolic link. Lstat // makes no attempt to follow the link. These semantics are identical to // os.Lstat. func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } // Readlink returns the destination of the named symbolic link. These // semantics are identical to os.Readlink. func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) }