Browse code

builder: prevent Dockerfile to leave build context

Signed-off-by: Tibor Vass <teabee89@gmail.com>

Tibor Vass authored on 2015/02/03 06:17:12
Showing 4 changed files
... ...
@@ -39,6 +39,7 @@ import (
39 39
 	"github.com/docker/docker/pkg/parsers/filters"
40 40
 	"github.com/docker/docker/pkg/promise"
41 41
 	"github.com/docker/docker/pkg/signal"
42
+	"github.com/docker/docker/pkg/symlink"
42 43
 	"github.com/docker/docker/pkg/term"
43 44
 	"github.com/docker/docker/pkg/timeutils"
44 45
 	"github.com/docker/docker/pkg/units"
... ...
@@ -147,36 +148,36 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
147 147
 			return err
148 148
 		}
149 149
 
150
-		var filename string       // path to Dockerfile
151
-		var origDockerfile string // used for error msg
150
+		filename := *dockerfileName // path to Dockerfile
152 151
 
153 152
 		if *dockerfileName == "" {
154 153
 			// No -f/--file was specified so use the default
155
-			origDockerfile = api.DefaultDockerfileName
156
-			*dockerfileName = origDockerfile
154
+			*dockerfileName = api.DefaultDockerfileName
157 155
 			filename = path.Join(absRoot, *dockerfileName)
158
-		} else {
159
-			origDockerfile = *dockerfileName
160
-			if filename, err = filepath.Abs(*dockerfileName); err != nil {
161
-				return err
162
-			}
156
+		}
163 157
 
164
-			// Verify that 'filename' is within the build context
165
-			if !strings.HasSuffix(absRoot, string(os.PathSeparator)) {
166
-				absRoot += string(os.PathSeparator)
167
-			}
168
-			if !strings.HasPrefix(filename, absRoot) {
169
-				return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", *dockerfileName, root)
170
-			}
158
+		origDockerfile := *dockerfileName // used for error msg
159
+
160
+		if filename, err = filepath.Abs(filename); err != nil {
161
+			return err
162
+		}
171 163
 
172
-			// Now reset the dockerfileName to be relative to the build context
173
-			*dockerfileName = filename[len(absRoot):]
164
+		// Verify that 'filename' is within the build context
165
+		filename, err = symlink.FollowSymlinkInScope(filename, absRoot)
166
+		if err != nil {
167
+			return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", origDockerfile, root)
168
+		}
169
+
170
+		// Now reset the dockerfileName to be relative to the build context
171
+		*dockerfileName, err = filepath.Rel(filename, absRoot)
172
+		if err != nil {
173
+			return err
174 174
 		}
175 175
 
176
-		if _, err = os.Stat(filename); os.IsNotExist(err) {
177
-			return fmt.Errorf("Can not locate Dockerfile: %s", origDockerfile)
176
+		if _, err = os.Lstat(filename); os.IsNotExist(err) {
177
+			return fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile)
178 178
 		}
179
-		var includes []string = []string{"."}
179
+		var includes = []string{"."}
180 180
 
181 181
 		excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore"))
182 182
 		if err != nil {
... ...
@@ -32,6 +32,7 @@ import (
32 32
 	"github.com/docker/docker/daemon"
33 33
 	"github.com/docker/docker/engine"
34 34
 	"github.com/docker/docker/pkg/fileutils"
35
+	"github.com/docker/docker/pkg/symlink"
35 36
 	"github.com/docker/docker/pkg/tarsum"
36 37
 	"github.com/docker/docker/registry"
37 38
 	"github.com/docker/docker/runconfig"
... ...
@@ -170,16 +171,12 @@ func (b *Builder) Run(context io.Reader) (string, error) {
170 170
 // Reads a Dockerfile from the current context. It assumes that the
171 171
 // 'filename' is a relative path from the root of the context
172 172
 func (b *Builder) readDockerfile(origFile string) error {
173
-	filename := filepath.Join(b.contextPath, origFile)
174
-
175
-	tmpDockerPath := filepath.Dir(filename) + string(os.PathSeparator)
176
-	tmpContextPath := filepath.Clean(b.contextPath) + string(os.PathSeparator)
177
-
178
-	if !strings.HasPrefix(tmpDockerPath, tmpContextPath) {
179
-		return fmt.Errorf("Dockerfile (%s) must be within the build context", origFile)
173
+	filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath)
174
+	if err != nil {
175
+		return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile)
180 176
 	}
181 177
 
182
-	fi, err := os.Stat(filename)
178
+	fi, err := os.Lstat(filename)
183 179
 	if os.IsNotExist(err) {
184 180
 		return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile)
185 181
 	}
... ...
@@ -307,13 +307,14 @@ func TestBuildApiDockerfilePath(t *testing.T) {
307 307
 	tw := tar.NewWriter(buffer)
308 308
 	defer tw.Close()
309 309
 
310
+	dockerfile := []byte("FROM busybox")
310 311
 	if err := tw.WriteHeader(&tar.Header{
311 312
 		Name: "Dockerfile",
312
-		Size: 11,
313
+		Size: int64(len(dockerfile)),
313 314
 	}); err != nil {
314 315
 		t.Fatalf("failed to write tar file header: %v", err)
315 316
 	}
316
-	if _, err := tw.Write([]byte("FROM ubuntu")); err != nil {
317
+	if _, err := tw.Write(dockerfile); err != nil {
317 318
 		t.Fatalf("failed to write tar file content: %v", err)
318 319
 	}
319 320
 	if err := tw.Close(); err != nil {
... ...
@@ -322,12 +323,46 @@ func TestBuildApiDockerfilePath(t *testing.T) {
322 322
 
323 323
 	out, err := sockRequestRaw("POST", "/build?dockerfile=../Dockerfile", buffer, "application/x-tar")
324 324
 	if err == nil {
325
-		t.Fatalf("Build was supposed to fail")
325
+		t.Fatalf("Build was supposed to fail: %s", out)
326 326
 	}
327 327
 
328 328
 	if !strings.Contains(string(out), "must be within the build context") {
329
-		t.Fatalf("Didn't complain about leaving build context")
329
+		t.Fatalf("Didn't complain about leaving build context: %s", out)
330 330
 	}
331 331
 
332 332
 	logDone("container REST API - check build w/bad Dockerfile path")
333 333
 }
334
+
335
+func TestBuildApiDockerfileSymlink(t *testing.T) {
336
+	// Test to make sure we stop people from trying to leave the
337
+	// build context when specifying a symlink as the path to the dockerfile
338
+	buffer := new(bytes.Buffer)
339
+	tw := tar.NewWriter(buffer)
340
+	defer tw.Close()
341
+
342
+	if err := tw.WriteHeader(&tar.Header{
343
+		Name:     "Dockerfile",
344
+		Typeflag: tar.TypeSymlink,
345
+		Linkname: "/etc/passwd",
346
+	}); err != nil {
347
+		t.Fatalf("failed to write tar file header: %v", err)
348
+	}
349
+	if err := tw.Close(); err != nil {
350
+		t.Fatalf("failed to close tar archive: %v", err)
351
+	}
352
+
353
+	out, err := sockRequestRaw("POST", "/build", buffer, "application/x-tar")
354
+	if err == nil {
355
+		t.Fatalf("Build was supposed to fail: %s", out)
356
+	}
357
+
358
+	// The reason the error is "Cannot locate specified Dockerfile" is because
359
+	// in the builder, the symlink is resolved within the context, therefore
360
+	// Dockerfile -> /etc/passwd becomes etc/passwd from the context which is
361
+	// a nonexistent file.
362
+	if !strings.Contains(string(out), "Cannot locate specified Dockerfile: Dockerfile") {
363
+		t.Fatalf("Didn't complain about leaving build context: %s", out)
364
+	}
365
+
366
+	logDone("container REST API - check build w/bad Dockerfile symlink path")
367
+}
... ...
@@ -4630,3 +4630,67 @@ func TestBuildFromOfficialNames(t *testing.T) {
4630 4630
 	}
4631 4631
 	logDone("build - from official names")
4632 4632
 }
4633
+
4634
+func TestBuildDockerfileOutsideContext(t *testing.T) {
4635
+	name := "testbuilddockerfileoutsidecontext"
4636
+	tmpdir, err := ioutil.TempDir("", name)
4637
+	if err != nil {
4638
+		t.Fatal(err)
4639
+	}
4640
+	defer os.RemoveAll(tmpdir)
4641
+	ctx := filepath.Join(tmpdir, "context")
4642
+	if err := os.MkdirAll(ctx, 0755); err != nil {
4643
+		t.Fatal(err)
4644
+	}
4645
+	if err := ioutil.WriteFile(filepath.Join(ctx, "Dockerfile"), []byte("FROM busybox"), 0644); err != nil {
4646
+		t.Fatal(err)
4647
+	}
4648
+	wd, err := os.Getwd()
4649
+	if err != nil {
4650
+		t.Fatal(err)
4651
+	}
4652
+	defer os.Chdir(wd)
4653
+	if err := os.Chdir(ctx); err != nil {
4654
+		t.Fatal(err)
4655
+	}
4656
+	if err := ioutil.WriteFile(filepath.Join(tmpdir, "outsideDockerfile"), []byte("FROM busbox"), 0644); err != nil {
4657
+		t.Fatal(err)
4658
+	}
4659
+	if err := os.Symlink("../outsideDockerfile", filepath.Join(ctx, "dockerfile1")); err != nil {
4660
+		t.Fatal(err)
4661
+	}
4662
+	if err := os.Symlink(filepath.Join(tmpdir, "outsideDockerfile"), filepath.Join(ctx, "dockerfile2")); err != nil {
4663
+		t.Fatal(err)
4664
+	}
4665
+	if err := os.Link("../outsideDockerfile", filepath.Join(ctx, "dockerfile3")); err != nil {
4666
+		t.Fatal(err)
4667
+	}
4668
+	if err := os.Link(filepath.Join(tmpdir, "outsideDockerfile"), filepath.Join(ctx, "dockerfile4")); err != nil {
4669
+		t.Fatal(err)
4670
+	}
4671
+	for _, dockerfilePath := range []string{
4672
+		"../outsideDockerfile",
4673
+		filepath.Join(ctx, "dockerfile1"),
4674
+		filepath.Join(ctx, "dockerfile2"),
4675
+		filepath.Join(ctx, "dockerfile3"),
4676
+		filepath.Join(ctx, "dockerfile4"),
4677
+	} {
4678
+		out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "build", "-t", name, "--no-cache", "-f", dockerfilePath, "."))
4679
+		if err == nil {
4680
+			t.Fatalf("Expected error with %s. Out: %s", dockerfilePath, out)
4681
+		}
4682
+		deleteImages(name)
4683
+	}
4684
+
4685
+	os.Chdir(tmpdir)
4686
+
4687
+	// Path to Dockerfile should be resolved relative to working directory, not relative to context.
4688
+	// There is a Dockerfile in the context, but since there is no Dockerfile in the current directory, the following should fail
4689
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "build", "-t", name, "--no-cache", "-f", "Dockerfile", ctx))
4690
+	if err == nil {
4691
+		t.Fatalf("Expected error. Out: %s", out)
4692
+	}
4693
+	deleteImages(name)
4694
+
4695
+	logDone("build - Dockerfile outside context")
4696
+}