-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
hardening config.Path() to disallow directory traversal #1720
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1720 +/- ##
==========================================
- Coverage 56.16% 56.14% -0.02%
==========================================
Files 306 306
Lines 21016 21028 +12
==========================================
+ Hits 11803 11807 +4
- Misses 8359 8366 +7
- Partials 854 855 +1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some notes / suggestions
cli/config/config.go
Outdated
return filepath.Join(append([]string{Dir()}, p...)...) | ||
func Path(p ...string) (string, error) { | ||
path := filepath.Join(append([]string{Dir()}, p...)...) | ||
if !strings.HasPrefix(path, filepath.Clean(Dir())+string(filepath.Separator)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if the filepath.Clean()
should actually be handled by SetDir()
(i.e., the output of Dir()
should be directly consumable)
Note that if we just want to check if it's outside of the base-dir, we could also simplify the check it self, and check if filepath.Join(p...)
results in a path with ..
as prefix; (go playground: https://play.golang.org/p/GtKy3dp_fqT)
package main
import (
"fmt"
"path/filepath"
)
func main() {
p := []string{"e", "..", "..", "f"}
f := filepath.Join(p...)
fmt.Println(f)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with just filepath.Join(p...)
is when p
is an absolute path then the join result will start with a slash or drive: https://play.golang.org/p/Nm0ZY_C1cO5.
Sorry, I should have (and will) add a test for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good one; didn't think about that case 👍
cli/config/config_test.go
Outdated
@@ -553,12 +553,19 @@ func TestConfigPath(t *testing.T) { | |||
oldDir := Dir() | |||
|
|||
SetDir("dummy1") | |||
f1 := Path("a", "b") | |||
f1, err1 := Path("a", "b") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this was already there, but there's no need to create a new fX
, errX
for each test-case, you could re-use the same one.
cli/config/config_test.go
Outdated
|
||
SetDir("dummy2") | ||
f2 := Path("c", "d") | ||
f2, err2 := Path("c", "d") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this test-case; I think this is exactly the same as the previous one (only different letters), so perhaps we should just remove this one;
func TestConfigPath(t *testing.T) {
f, err := Path("a", "b")
assert.Equal(t, f, filepath.Join(Dir(), "a", "b"))
assert.NilError(t, err)
f, err = Path("e", "..", "..", "f")
assert.Equal(t, f, "")
assert.ErrorContains(t, err, "is outside of root config directory '"+Dir()+"'")
}
We could make this a simple subtest, but that may be a bit over-the-top if we won't be adding more test-cases;
func TestConfigPath(t *testing.T) {
for _, tc := range []struct {
name string
path []string
expected string
expectedErr string
}{
{
name: "valid",
path: []string{"a", "b"},
expected: filepath.Join(Dir(), "a", "b"),
},
{
name: "invalid",
path: []string{"e", "..", "..", "f"},
expectedErr: "is outside of root config directory '" + Dir() + "'",
},
} {
t.Run(tc.name, func(t *testing.T) {
f, err := Path(tc.path...)
assert.Equal(t, f, tc.expected)
if tc.expectedErr == "" {
assert.NilError(t, err)
} else {
assert.ErrorContains(t, err, tc.expectedErr)
}
})
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Thanks for the example. I've altered the tests to match this and added a few extra cases for testing absolute paths
cli/config/config_test.go
Outdated
@@ -553,12 +553,19 @@ func TestConfigPath(t *testing.T) { | |||
oldDir := Dir() | |||
|
|||
SetDir("dummy1") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, looking at this, and wondering if we should do this as part of this test (perhaps it was done to increase coverage at the time, but I don't think we need to change the path here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added tests that will also check functionality when the Dir() is set to an absolute path so I've kept this part
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, left a nit and a question but still fine.
cli-plugins/manager/manager_test.go
Outdated
expected = append(expected, defaultSystemPluginDirs...) | ||
|
||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":")) | ||
pluginDirs1, err1 := getPluginDirs(cli) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can (and should) reuse pluginDir
and err
here and the same for the 2
s below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed; I see this PR still needs some squashing, so if you could make that change as part of that 👍
cli/config/config_test.go
Outdated
}, | ||
{ | ||
name: "invalid_absolute_path_windows", | ||
path: []string{"C:/", "..", ".."}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this test DTRT when GOOS=linux
(or !=windows
generally)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL DTRT - had to look that one up 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're testing for these situations, we also might need a test for
C:\
(backslash)- Embedded travering (for better name);
[]string{"a", "../../..", "b"}
(although we might enter the area of testing Go stdlib itself)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I ask is that path/filepath
is (by definition) platform specific, so on Unix-y platforms it doesn't treat e.g .C:/
as special (I expect it thinks it is a directory in the root called C:
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, on second look this won't work as the build is on unix. Probably easiest to simply remove the "windows" tests for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Left some comments inline, also:
There's three commits in this PR. Please squash some of those commits before merging (at least the second one is a touch-up commit).
The "clean config dir on set and add config path tests for root paths and" commit contains the change to add filepath.Clean()
to SetDir()
. It's a small change, so I'd be ok with squashing it with the other changes, but if you do want to keep it separate, it would be better to make that the first commit, with only that change;
func SetDir(dir string) {
- configDir = dir
+ configDir = filepath.Clean(dir)
}
Also, small nit: try to make the commit message's first line fit within 50..72 characters so that it doesn't wrap (requires some creativity at times 😅)
cli/config/config_test.go
Outdated
}, | ||
{ | ||
name: "invalid_absolute_path_windows", | ||
path: []string{"C:/", "..", ".."}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL DTRT - had to look that one up 😅
cli-plugins/manager/manager_test.go
Outdated
expected := []string{config.Path("cli-plugins")} | ||
pluginDir, err := config.Path("cli-plugins") | ||
assert.NilError(t, err) | ||
expected := []string{pluginDir} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could be combined to a single line
expected := []string{pluginDir} | |
expected := append([]string{pluginDir}, defaultSystemPluginDirs...) |
cli-plugins/manager/manager_test.go
Outdated
expected = append(expected, defaultSystemPluginDirs...) | ||
|
||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":")) | ||
pluginDirs1, err1 := getPluginDirs(cli) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed; I see this PR still needs some squashing, so if you could make that change as part of that 👍
cli/config/config_test.go
Outdated
}, | ||
{ | ||
name: "invalid_absolute_path_windows", | ||
path: []string{"C:/", "..", ".."}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're testing for these situations, we also might need a test for
C:\
(backslash)- Embedded travering (for better name);
[]string{"a", "../../..", "b"}
(although we might enter the area of testing Go stdlib itself)
43f091c
to
ddb1183
Compare
Signed-off-by: Nick Adcock <nick.adcock@docker.com>
ddb1183
to
ff51b0d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
- What I did
Resolving issue raised in comment, removing the ability to get paths from above the config directory's root path
- How I did it
Checks whether the resolved path is underneath the root config directory using a strings.HasPrefix on the cleaned paths. Returns an error if it's not.
- How to verify it
Added unit test for attempting to traverse outside of the config directory
- Description for the changelog