Skip to content

Commit d62030f

Browse files
author
g.lenda
committed
split tests, so that tests for symlinks do not run on plan 9.
mv common variables and functions into common.go Tests do not currently pass on Plan 9. I'm working on it. I want to see if this mild restructure is OK.
1 parent 2ee2c46 commit d62030f

File tree

4 files changed

+333
-294
lines changed

4 files changed

+333
-294
lines changed

common.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
2+
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package securejoin
7+
8+
import (
9+
"errors"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"syscall"
14+
)
15+
16+
// IsNotExist tells you if err is an error that implies that either the path
17+
// accessed does not exist (or path components don't exist). This is
18+
// effectively a more broad version of [os.IsNotExist].
19+
func IsNotExist(err error) bool {
20+
// Check that it's not actually an ENOTDIR, which in some cases is a more
21+
// convoluted case of ENOENT (usually involving weird paths).
22+
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
23+
}
24+
25+
// errUnsafeRoot is returned if the user provides SecureJoinVFS with a path
26+
// that contains ".." components.
27+
var errUnsafeRoot = errors.New("root path provided to SecureJoin contains '..' components")
28+
29+
// hasDotDot checks if the path contains ".." components in a platform-agnostic
30+
// way.
31+
func hasDotDot(path string) bool {
32+
// If we are on Windows, strip any volume letters. It turns out that
33+
// C:..\foo may (or may not) be a valid pathname and we need to handle that
34+
// leading "..".
35+
path = stripVolume(path)
36+
// Look for "/../" in the path, but we need to handle leading and trailing
37+
// ".."s by adding separators. Doing this with filepath.Separator is ugly
38+
// so just convert to Unix-style "/" first.
39+
path = filepath.ToSlash(path)
40+
return strings.Contains("/"+path+"/", "/../")
41+
}
42+
43+
// stripVolume just gets rid of the Windows volume included in a path. Based on
44+
// some godbolt tests, the Go compiler is smart enough to make this a no-op on
45+
// Linux.
46+
func stripVolume(path string) string {
47+
return path[len(filepath.VolumeName(path)):]
48+
}
49+

join.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,6 @@ import (
1717

1818
const maxSymlinkLimit = 255
1919

20-
// IsNotExist tells you if err is an error that implies that either the path
21-
// accessed does not exist (or path components don't exist). This is
22-
// effectively a more broad version of [os.IsNotExist].
23-
func IsNotExist(err error) bool {
24-
// Check that it's not actually an ENOTDIR, which in some cases is a more
25-
// convoluted case of ENOENT (usually involving weird paths).
26-
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
27-
}
28-
29-
// errUnsafeRoot is returned if the user provides SecureJoinVFS with a path
30-
// that contains ".." components.
31-
var errUnsafeRoot = errors.New("root path provided to SecureJoin contains '..' components")
32-
33-
// stripVolume just gets rid of the Windows volume included in a path. Based on
34-
// some godbolt tests, the Go compiler is smart enough to make this a no-op on
35-
// Linux.
36-
func stripVolume(path string) string {
37-
return path[len(filepath.VolumeName(path)):]
38-
}
39-
40-
// hasDotDot checks if the path contains ".." components in a platform-agnostic
41-
// way.
42-
func hasDotDot(path string) bool {
43-
// If we are on Windows, strip any volume letters. It turns out that
44-
// C:..\foo may (or may not) be a valid pathname and we need to handle that
45-
// leading "..".
46-
path = stripVolume(path)
47-
// Look for "/../" in the path, but we need to handle leading and trailing
48-
// ".."s by adding separators. Doing this with filepath.Separator is ugly
49-
// so just convert to Unix-style "/" first.
50-
path = filepath.ToSlash(path)
51-
return strings.Contains("/"+path+"/", "/../")
52-
}
53-
5420
// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
5521
// that the returned path is guaranteed to be scoped inside the provided root
5622
// path (when evaluated). Any symbolic links in the path are evaluated with the

join_test.go

Lines changed: 1 addition & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !plan9
6-
75
package securejoin
86

97
import (
@@ -15,7 +13,6 @@ import (
1513
"testing"
1614

1715
"github.com/stretchr/testify/assert"
18-
"github.com/stretchr/testify/require"
1916
)
2017

2118
// TODO: These tests won't work on plan9 because it doesn't have symlinks, and
@@ -26,64 +23,9 @@ type input struct {
2623
expected string
2724
}
2825

29-
func expandedTempDir(t *testing.T) string {
30-
dir := t.TempDir()
31-
dir, err := filepath.EvalSymlinks(dir)
32-
require.NoError(t, err)
33-
return dir
34-
}
35-
36-
// Test basic handling of symlink expansion.
37-
func TestSymlink(t *testing.T) {
38-
dir := expandedTempDir(t)
39-
40-
symlink(t, "somepath", filepath.Join(dir, "etc"))
41-
symlink(t, "../../../../../../../../../../../../../etc", filepath.Join(dir, "etclink"))
42-
symlink(t, "/../../../../../../../../../../../../../etc/passwd", filepath.Join(dir, "passwd"))
43-
44-
rootOrVol := string(filepath.Separator)
45-
if vol := filepath.VolumeName(dir); vol != "" {
46-
rootOrVol = vol + rootOrVol
47-
}
48-
49-
tc := []input{
50-
// Make sure that expansion with a root of '/' proceeds in the expected fashion.
51-
{rootOrVol, filepath.Join(dir, "passwd"), filepath.Join(rootOrVol, "etc", "passwd")},
52-
{rootOrVol, filepath.Join(dir, "etclink"), filepath.Join(rootOrVol, "etc")},
53-
54-
{rootOrVol, filepath.Join(dir, "etc"), filepath.Join(dir, "somepath")},
55-
// Now test scoped expansion.
56-
{dir, "passwd", filepath.Join(dir, "somepath", "passwd")},
57-
{dir, "etclink", filepath.Join(dir, "somepath")},
58-
{dir, "etc", filepath.Join(dir, "somepath")},
59-
{dir, "etc/test", filepath.Join(dir, "somepath", "test")},
60-
{dir, "etc/test/..", filepath.Join(dir, "somepath")},
61-
}
62-
63-
for _, test := range tc {
64-
got, err := SecureJoin(test.root, test.unsafe)
65-
if err != nil {
66-
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
67-
continue
68-
}
69-
// This is only for OS X, where /etc is a symlink to /private/etc. In
70-
// principle, SecureJoin(/, pth) is the same as EvalSymlinks(pth) in
71-
// the case where the path exists.
72-
if test.root == "/" {
73-
if expected, err := filepath.EvalSymlinks(test.expected); err == nil {
74-
test.expected = expected
75-
}
76-
}
77-
if got != test.expected {
78-
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
79-
continue
80-
}
81-
}
82-
}
83-
8426
// In a path without symlinks, SecureJoin is equivalent to Clean+Join.
8527
func TestNoSymlink(t *testing.T) {
86-
dir := expandedTempDir(t)
28+
dir := t.TempDir()
8729

8830
tc := []input{
8931
{dir, "somepath", filepath.Join(dir, "somepath")},
@@ -114,92 +56,6 @@ func TestNoSymlink(t *testing.T) {
11456
}
11557
}
11658

117-
// Make sure that .. is **not** expanded lexically.
118-
func TestNonLexical(t *testing.T) {
119-
dir := expandedTempDir(t)
120-
121-
mkdirAll(t, filepath.Join(dir, "subdir"), 0o755)
122-
mkdirAll(t, filepath.Join(dir, "cousinparent", "cousin"), 0o755)
123-
symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link"))
124-
symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2"))
125-
symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3"))
126-
127-
for _, test := range []input{
128-
{dir, "subdir", filepath.Join(dir, "subdir")},
129-
{dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
130-
{dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
131-
{dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
132-
{dir, "subdir/../test", filepath.Join(dir, "test")},
133-
// This is the divergence from a simple filepath.Clean implementation.
134-
{dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")},
135-
{dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")},
136-
{dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")},
137-
} {
138-
got, err := SecureJoin(test.root, test.unsafe)
139-
if err != nil {
140-
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
141-
continue
142-
}
143-
if got != test.expected {
144-
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
145-
continue
146-
}
147-
}
148-
}
149-
150-
// Make sure that symlink loops result in errors.
151-
func TestSymlinkLoop(t *testing.T) {
152-
dir := expandedTempDir(t)
153-
154-
mkdirAll(t, filepath.Join(dir, "subdir"), 0o755)
155-
symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "subdir", "link"))
156-
symlink(t, "/subdir/link", filepath.Join(dir, "path"))
157-
symlink(t, "/../../../../../../../../../../../../../../../../self", filepath.Join(dir, "self"))
158-
159-
for _, test := range []struct {
160-
root, unsafe string
161-
}{
162-
{dir, "subdir/link"},
163-
{dir, "path"},
164-
{dir, "../../path"},
165-
{dir, "subdir/link/../.."},
166-
{dir, "../../../../../../../../../../../../../../../../subdir/link/../../../../../../../../../../../../../../../.."},
167-
{dir, "self"},
168-
{dir, "self/.."},
169-
{dir, "/../../../../../../../../../../../../../../../../self/.."},
170-
{dir, "/self/././.."},
171-
} {
172-
got, err := SecureJoin(test.root, test.unsafe)
173-
if !errors.Is(err, syscall.ELOOP) {
174-
t.Errorf("securejoin(%q, %q): expected ELOOP, got %q & %v", test.root, test.unsafe, got, err)
175-
continue
176-
}
177-
}
178-
}
179-
180-
// Make sure that ENOTDIR is correctly handled.
181-
func TestEnotdir(t *testing.T) {
182-
dir := expandedTempDir(t)
183-
184-
mkdirAll(t, filepath.Join(dir, "subdir"), 0o755)
185-
writeFile(t, filepath.Join(dir, "notdir"), []byte("I am not a directory!"), 0o755)
186-
symlink(t, "/../../../notdir/somechild", filepath.Join(dir, "subdir", "link"))
187-
188-
for _, test := range []struct {
189-
root, unsafe string
190-
}{
191-
{dir, "subdir/link"},
192-
{dir, "notdir"},
193-
{dir, "notdir/child"},
194-
} {
195-
_, err := SecureJoin(test.root, test.unsafe)
196-
if err != nil {
197-
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
198-
continue
199-
}
200-
}
201-
}
202-
20359
// Some silly tests to make sure that all error types are correctly handled.
20460
func TestIsNotExist(t *testing.T) {
20561
for _, test := range []struct {
@@ -224,121 +80,6 @@ func TestIsNotExist(t *testing.T) {
22480
}
22581
}
22682

227-
type mockVFS struct {
228-
lstat func(path string) (os.FileInfo, error)
229-
readlink func(path string) (string, error)
230-
}
231-
232-
func (m mockVFS) Lstat(path string) (os.FileInfo, error) { return m.lstat(path) }
233-
func (m mockVFS) Readlink(path string) (string, error) { return m.readlink(path) }
234-
235-
// Make sure that SecureJoinVFS actually does use the given VFS interface.
236-
func TestSecureJoinVFS(t *testing.T) {
237-
dir := expandedTempDir(t)
238-
239-
mkdirAll(t, filepath.Join(dir, "subdir"), 0o755)
240-
mkdirAll(t, filepath.Join(dir, "cousinparent", "cousin"), 0o755)
241-
symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link"))
242-
symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2"))
243-
symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3"))
244-
245-
for _, test := range []input{
246-
{dir, "subdir", filepath.Join(dir, "subdir")},
247-
{dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
248-
{dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
249-
{dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
250-
{dir, "subdir/../test", filepath.Join(dir, "test")},
251-
// This is the divergence from a simple filepath.Clean implementation.
252-
{dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")},
253-
{dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")},
254-
{dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")},
255-
} {
256-
var nLstat, nReadlink int
257-
mock := mockVFS{
258-
lstat: func(path string) (os.FileInfo, error) { nLstat++; return os.Lstat(path) },
259-
readlink: func(path string) (string, error) { nReadlink++; return os.Readlink(path) },
260-
}
261-
262-
got, err := SecureJoinVFS(test.root, test.unsafe, mock)
263-
if err != nil {
264-
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
265-
continue
266-
}
267-
if got != test.expected {
268-
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
269-
continue
270-
}
271-
if nLstat == 0 && nReadlink == 0 {
272-
t.Errorf("securejoin(%q, %q): expected to use either lstat or readlink, neither were used", test.root, test.unsafe)
273-
}
274-
}
275-
}
276-
277-
// Make sure that SecureJoinVFS actually does use the given VFS interface, and
278-
// that errors are correctly propagated.
279-
func TestSecureJoinVFSErrors(t *testing.T) {
280-
var (
281-
lstatErr = errors.New("lstat error")
282-
readlinkErr = errors.New("readlink err")
283-
)
284-
285-
dir := expandedTempDir(t)
286-
287-
// Make a link.
288-
symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "link"))
289-
290-
// Define some fake mock functions.
291-
lstatFailFn := func(string) (os.FileInfo, error) { return nil, lstatErr }
292-
readlinkFailFn := func(string) (string, error) { return "", readlinkErr }
293-
294-
// Make sure that the set of {lstat, readlink} failures do propagate.
295-
for idx, test := range []struct {
296-
vfs VFS
297-
expected []error
298-
}{
299-
{
300-
expected: []error{nil},
301-
vfs: mockVFS{
302-
lstat: os.Lstat,
303-
readlink: os.Readlink,
304-
},
305-
},
306-
{
307-
expected: []error{lstatErr},
308-
vfs: mockVFS{
309-
lstat: lstatFailFn,
310-
readlink: os.Readlink,
311-
},
312-
},
313-
{
314-
expected: []error{readlinkErr},
315-
vfs: mockVFS{
316-
lstat: os.Lstat,
317-
readlink: readlinkFailFn,
318-
},
319-
},
320-
{
321-
expected: []error{lstatErr, readlinkErr},
322-
vfs: mockVFS{
323-
lstat: lstatFailFn,
324-
readlink: readlinkFailFn,
325-
},
326-
},
327-
} {
328-
_, err := SecureJoinVFS(dir, "link", test.vfs)
329-
330-
success := false
331-
for _, exp := range test.expected {
332-
if errors.Is(err, exp) {
333-
success = true
334-
}
335-
}
336-
if !success {
337-
t.Errorf("SecureJoinVFS.mock%d: expected to get lstatError, got %v", idx, err)
338-
}
339-
}
340-
}
341-
34283
func TestUncleanRoot(t *testing.T) {
34384
root := t.TempDir()
34485

0 commit comments

Comments
 (0)