Skip to content

Commit f3a512c

Browse files
committed
merge #43 into cyphar/filepath-securejoin:main
Aleksa Sarai (1): join: return an error if root is unclean path LGTMs: cyphar
2 parents 1be4136 + bc750ad commit f3a512c

File tree

3 files changed

+42
-1
lines changed

3 files changed

+42
-1
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
66

77
## [Unreleased] ##
88

9+
### Breaking ####
10+
- `SecureJoin(VFS)` will now return an error if the provided `root` is not a
11+
`filepath.Clean`'d path.
12+
13+
While it is ultimately the responsibility of the caller to ensure the root is
14+
a safe path to use, passing a path like `/symlink/..` as a root would result
15+
in the `SecureJoin`'d path being placed in `/` even though `/symlink/..`
16+
might be a different directory, and so we should more strongly discourage
17+
such usage.
18+
19+
All major users of `securejoin.SecureJoin` already ensure that the paths they
20+
provide are safe (and this is ultimately a question of user error), but
21+
removing this foot-gun is probably a good idea. Of course, this is
22+
necessarily a breaking API change (though we expect no real users to be
23+
affected by it).
24+
925
## [0.3.6] - 2024-12-17 ##
1026

1127
### Compatibility ###

join.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
2-
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
2+
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
33
// Use of this source code is governed by a BSD-style
44
// license that can be found in the LICENSE file.
55

@@ -24,6 +24,10 @@ func IsNotExist(err error) bool {
2424
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
2525
}
2626

27+
// errUncleanRoot is returned if the user provides SecureJoinVFS with a path
28+
// that is not filepath.Clean'd.
29+
var errUncleanRoot = errors.New("root path provided to SecureJoin was not filepath.Clean")
30+
2731
// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
2832
// that the returned path is guaranteed to be scoped inside the provided root
2933
// path (when evaluated). Any symbolic links in the path are evaluated with the
@@ -46,7 +50,21 @@ func IsNotExist(err error) bool {
4650
// provided via direct input or when evaluating symlinks. Therefore:
4751
//
4852
// "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt"
53+
//
54+
// If the provided root is not [filepath.Clean] then an error will be returned,
55+
// as such root paths are bordering on somewhat unsafe and using such paths is
56+
// not best practice. We also strongly suggest that any root path is first
57+
// fully resolved using [filepath.EvalSymlinks] or otherwise constructed to
58+
// avoid containing symlink components. Of course, the root also *must not* be
59+
// attacker-controlled.
4960
func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
61+
// The root path needs to be clean, otherwise when we join the subpath we
62+
// will end up with a weird path. We could work around this but users
63+
// should not be giving us unclean paths in the first place.
64+
if filepath.Clean(root) != root {
65+
return "", errUncleanRoot
66+
}
67+
5068
// Use the os.* VFS implementation if none was specified.
5169
if vfs == nil {
5270
vfs = osVFS{}

join_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,10 @@ func TestSecureJoinVFSErrors(t *testing.T) {
390390
}
391391
}
392392
}
393+
394+
func TestUncleanRoot(t *testing.T) {
395+
safePath, err := SecureJoin("/foo/..", "bar/baz")
396+
if !errors.Is(err, errUncleanRoot) {
397+
t.Errorf("SecureJoin with non-clean path should return errUncleanRoot, instead got (%q, %v)", safePath, err)
398+
}
399+
}

0 commit comments

Comments
 (0)