Skip to content

hardening config.Path() to disallow directory traversal - #1720

Merged
silvin-lubecki merged 1 commit into
docker:masterfrom
zappy-shu:harden-config-path
Mar 10, 2019
Merged

hardening config.Path() to disallow directory traversal#1720
silvin-lubecki merged 1 commit into
docker:masterfrom
zappy-shu:harden-config-path

Conversation

@zappy-shu

Copy link
Copy Markdown

- 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

  • hardening config.Path() to disallow directory traversal

@codecov-io

codecov-io commented Mar 6, 2019

Copy link
Copy Markdown

Codecov Report

Merging #1720 into master will decrease coverage by 0.01%.
The diff coverage is 50%.

@@ 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

@thaJeztahthaJeztah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some notes / suggestions

Comment threadcli/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)) {

Copy link
Copy Markdown
Member

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"
)
funcmain() {
p:= []string{"e", "..", "..", "f"}
f:=filepath.Join(p...)
fmt.Println(f)
}

Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown
Member

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 👍

Comment threadcli/config/config_test.go Outdated

SetDir("dummy1")
f1 := Path("a", "b")
f1, err1 := Path("a", "b")

Copy link
Copy Markdown
Member

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.

Comment threadcli/config/config_test.go Outdated

SetDir("dummy2")
f2 := Path("c", "d")
f2, err2 := Path("c", "d")

Copy link
Copy Markdown
Member

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;

funcTestConfigPath(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;

funcTestConfigPath(t*testing.T) {
for_, tc:=range []struct {
namestringpath []stringexpectedstringexpectedErrstring
}{
{
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)
iftc.expectedErr=="" {
assert.NilError(t, err)
} else {
assert.ErrorContains(t, err, tc.expectedErr)
}
})
}
}

Copy link
Copy Markdown
Author

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

Comment threadcli/config/config_test.go Outdated
@@ -553,12 +553,19 @@ func TestConfigPath(t *testing.T) {
oldDir := Dir()

SetDir("dummy1")

Copy link
Copy Markdown
Member

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).

Copy link
Copy Markdown
Author

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

ijc
ijc approved these changes Mar 7, 2019

@ijcijc left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadcli-plugins/manager/manager_test.go Outdated
expected = append(expected, defaultSystemPluginDirs...)

assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":"))
pluginDirs1, err1 := getPluginDirs(cli)

Copy link
Copy Markdown
Contributor

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 2s below.

Copy link
Copy Markdown
Member

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 👍

Comment threadcli/config/config_test.go Outdated
},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},

Copy link
Copy Markdown
Contributor

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)?

Copy link
Copy Markdown
Member

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 😅

Copy link
Copy Markdown
Member

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)

Copy link
Copy Markdown
Contributor

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:).

Copy link
Copy Markdown
Author

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

@thaJeztahthaJeztah left a comment

Copy link
Copy Markdown
Member

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 😅)

Comment threadcli/config/config_test.go Outdated
},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},

Copy link
Copy Markdown
Member

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 😅

Comment threadcli-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}

Copy link
Copy Markdown
Member

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

Suggested change
expected:= []string{pluginDir}
expected:=append([]string{pluginDir}, defaultSystemPluginDirs...)

Comment threadcli-plugins/manager/manager_test.go Outdated
expected = append(expected, defaultSystemPluginDirs...)

assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":"))
pluginDirs1, err1 := getPluginDirs(cli)

Copy link
Copy Markdown
Member

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 👍

Comment threadcli/config/config_test.go Outdated
},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},

Copy link
Copy Markdown
Member

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)

@zappy-shu
zappy-shuforce-pushed the harden-config-path branch 3 times, most recently from 43f091c to ddb1183CompareMarch 7, 2019 14:29
Signed-off-by: Nick Adcock <nick.adcock@docker.com>

@thaJeztahthaJeztah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@silvin-lubeckisilvin-lubecki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@silvin-lubecki
silvin-lubecki merged commit fdb0ef7 into docker:masterMar 10, 2019
@GordonTheTurtleGordonTheTurtle added this to the 19.03.0 milestone Mar 10, 2019
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@zappy-shu@codecov-io@thaJeztah@ijc@silvin-lubecki@GordonTheTurtle