Skip to content

Commit 7f8e0d1

Browse files
committed
Prevent overwriting irregular files (cp, save, export commands)
Signed-off-by: Philipp Schmied <pschmied@schutzwerk.com>
1 parent 3a6f8b6 commit 7f8e0d1

File tree

7 files changed

+79
-14
lines changed

7 files changed

+79
-14
lines changed

cli/command/container/cp.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
128128
}
129129
}
130130

131+
if err := command.ValidateOutputPath(dstPath); err != nil {
132+
return err
133+
}
134+
131135
client := dockerCli.Client()
132136
// if client requests to follow symbol link, then must decide target file to be copied
133137
var rebaseName string
@@ -209,6 +213,11 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
209213
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
210214
}
211215

216+
// Validate the destination path
217+
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
218+
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
219+
}
220+
212221
// Ignore any error and assume that the parent directory of the destination
213222
// path exists, in which case the copy may still succeed. If there is any
214223
// type of conflict (e.g., non-directory overwriting an existing directory

cli/command/container/cp_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,12 @@ func TestSplitCpArg(t *testing.T) {
190190
})
191191
}
192192
}
193+
194+
func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
195+
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
196+
cli := test.NewFakeCli(nil)
197+
err := runCopy(cli, options)
198+
assert.Assert(t, err != nil)
199+
expected := `"/dev/random" must be a directory or a regular file`
200+
assert.ErrorContains(t, err, expected)
201+
}

cli/command/container/export.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ func runExport(dockerCli command.Cli, opts exportOptions) error {
4141
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
4242
}
4343

44+
if err := command.ValidateOutputPath(opts.output); err != nil {
45+
return errors.Wrap(err, "failed to export container")
46+
}
47+
4448
clnt := dockerCli.Client()
4549

4650
responseBody, err := clnt.ContainerExport(context.Background(), opts.container)

cli/command/container/export_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,19 @@ func TestContainerExportOutputToFile(t *testing.T) {
3131

3232
assert.Assert(t, fs.Equal(dir.Path(), expected))
3333
}
34+
35+
func TestContainerExportOutputToIrregularFile(t *testing.T) {
36+
cli := test.NewFakeCli(&fakeClient{
37+
containerExportFunc: func(container string) (io.ReadCloser, error) {
38+
return ioutil.NopCloser(strings.NewReader("foo")), nil
39+
},
40+
})
41+
cmd := NewExportCommand(cli)
42+
cmd.SetOutput(ioutil.Discard)
43+
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
44+
45+
err := cmd.Execute()
46+
assert.Assert(t, err != nil)
47+
expected := `"/dev/random" must be a directory or a regular file`
48+
assert.ErrorContains(t, err, expected)
49+
}

cli/command/image/save.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package image
33
import (
44
"context"
55
"io"
6-
"os"
7-
"path/filepath"
86

97
"github.com/docker/cli/cli"
108
"github.com/docker/cli/cli/command"
@@ -44,7 +42,7 @@ func RunSave(dockerCli command.Cli, opts saveOptions) error {
4442
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
4543
}
4644

47-
if err := validateOutputPath(opts.output); err != nil {
45+
if err := command.ValidateOutputPath(opts.output); err != nil {
4846
return errors.Wrap(err, "failed to save image")
4947
}
5048

@@ -61,13 +59,3 @@ func RunSave(dockerCli command.Cli, opts saveOptions) error {
6159

6260
return command.CopyToFile(opts.output, responseBody)
6361
}
64-
65-
func validateOutputPath(path string) error {
66-
dir := filepath.Dir(path)
67-
if dir != "" && dir != "." {
68-
if _, err := os.Stat(dir); os.IsNotExist(err) {
69-
return errors.Errorf("unable to validate output path: directory %q does not exist", dir)
70-
}
71-
}
72-
return nil
73-
}

cli/command/image/save_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ func TestNewSaveCommandErrors(t *testing.T) {
4444
{
4545
name: "output directory does not exist",
4646
args: []string{"-o", "fakedir/out.tar", "arg1"},
47-
expectedError: "failed to save image: unable to validate output path: directory \"fakedir\" does not exist",
47+
expectedError: "failed to save image: invalid output path: directory \"fakedir\" does not exist",
48+
},
49+
{
50+
name: "output file is irregular",
51+
args: []string{"-o", "/dev/null", "arg1"},
52+
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
4853
},
4954
}
5055
for _, tc := range testCases {

cli/command/utils.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/docker/docker/api/types/filters"
1313
"github.com/docker/docker/pkg/system"
14+
"github.com/pkg/errors"
1415
"github.com/spf13/pflag"
1516
)
1617

@@ -125,3 +126,36 @@ func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
125126
flags.SetAnnotation("platform", "version", []string{"1.32"})
126127
flags.SetAnnotation("platform", "experimental", nil)
127128
}
129+
130+
// ValidateOutputPath validates the output paths of the `export` and `save` commands.
131+
func ValidateOutputPath(path string) error {
132+
dir := filepath.Dir(path)
133+
if dir != "" && dir != "." {
134+
if _, err := os.Stat(dir); os.IsNotExist(err) {
135+
return errors.Errorf("invalid output path: directory %q does not exist", dir)
136+
}
137+
}
138+
// check whether `path` points to a regular file
139+
// (if the path exists and doesn't point to a directory)
140+
if fileInfo, err := os.Stat(path); !os.IsNotExist(err) {
141+
if fileInfo.Mode().IsDir() || fileInfo.Mode().IsRegular() {
142+
return nil
143+
}
144+
145+
if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil {
146+
return errors.Wrap(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
147+
}
148+
}
149+
return nil
150+
}
151+
152+
// ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
153+
// helper to `ValidateOutputPath`
154+
func ValidateOutputPathFileMode(fileMode os.FileMode) error {
155+
if fileMode&os.ModeDevice != 0 {
156+
return errors.New("got a device")
157+
} else if fileMode&os.ModeIrregular != 0 {
158+
return errors.New("got an irregular file")
159+
}
160+
return nil
161+
}

0 commit comments

Comments
 (0)