Skip to content
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

Merged
merged 1 commit into from
Mar 10, 2019

Conversation

zappy-shu
Copy link
Contributor

- 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
Copy link

codecov-io commented Mar 6, 2019

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

Copy link
Member

@thaJeztah thaJeztah left a 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

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

func main() {
	p := []string{"e", "..", "..", "f"}
	f := filepath.Join(p...)

	fmt.Println(f)
}

Copy link
Contributor 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
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 👍

@@ -553,12 +553,19 @@ func TestConfigPath(t *testing.T) {
oldDir := Dir()

SetDir("dummy1")
f1 := Path("a", "b")
f1, err1 := Path("a", "b")
Copy link
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.


SetDir("dummy2")
f2 := Path("c", "d")
f2, err2 := Path("c", "d")
Copy link
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;

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

Copy link
Contributor 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

@@ -553,12 +553,19 @@ func TestConfigPath(t *testing.T) {
oldDir := Dir()

SetDir("dummy1")
Copy link
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
Contributor 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

Copy link
Contributor

@ijc ijc left a 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.

expected = append(expected, defaultSystemPluginDirs...)

assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":"))
pluginDirs1, err1 := getPluginDirs(cli)
Copy link
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
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 👍

},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},
Copy link
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
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
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
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
Contributor 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

Copy link
Member

@thaJeztah thaJeztah left a 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 😅)

},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},
Copy link
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 😅

expected := []string{config.Path("cli-plugins")}
pluginDir, err := config.Path("cli-plugins")
assert.NilError(t, err)
expected := []string{pluginDir}
Copy link
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...)

expected = append(expected, defaultSystemPluginDirs...)

assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":"))
pluginDirs1, err1 := getPluginDirs(cli)
Copy link
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 👍

},
{
name: "invalid_absolute_path_windows",
path: []string{"C:/", "..", ".."},
Copy link
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-shu force-pushed the harden-config-path branch 3 times, most recently from 43f091c to ddb1183 Compare March 7, 2019 14:29
Signed-off-by: Nick Adcock <nick.adcock@docker.com>
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@silvin-lubecki silvin-lubecki left a comment

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:master Mar 10, 2019
@GordonTheTurtle GordonTheTurtle added this to the 19.03.0 milestone Mar 10, 2019
Sign up for free to 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.

None yet

6 participants