|
1 | 1 | package workspace |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "archive/tar" |
4 | 5 | "context" |
| 6 | + "io" |
5 | 7 | "os" |
6 | 8 | "path/filepath" |
7 | 9 | "strings" |
@@ -39,13 +41,10 @@ func TestVolumeWorkspaceCreator(t *testing.T) { |
39 | 41 | mockAdditionalFilePaths: map[string]string{}, |
40 | 42 | } |
41 | 43 | for _, name := range []string{".gitignore", "another-file"} { |
42 | | - // Since we don't read the files and mock the Docker commands, |
43 | | - // we don't need to create them. |
44 | | - path := filepath.Join(os.TempDir(), "additional-file"+name) |
45 | | - // Instead we create a real-looking path that we sanitize so |
46 | | - // it doesn't trip up the globbing expecations below: |
47 | | - path = strings.ReplaceAll(path, string(os.PathSeparator), "-") |
48 | | - |
| 44 | + path := filepath.Join(t.TempDir(), "additional-file"+name) |
| 45 | + if err := os.WriteFile(path, []byte(name), 0600); err != nil { |
| 46 | + t.Fatal(err) |
| 47 | + } |
49 | 48 | archiveWithAdditionalFiles.mockAdditionalFilePaths[name] = path |
50 | 49 | } |
51 | 50 |
|
@@ -336,15 +335,11 @@ func TestVolumeWorkspaceCreator(t *testing.T) { |
336 | 335 | expect.Success, |
337 | 336 | "docker", "run", "--rm", "--init", |
338 | 337 | "--workdir", "/work", |
| 338 | + "--mount", "type=bind,source=*,target=/tmp/additional-files.tar,ro", |
339 | 339 | "--user", "0:0", |
340 | 340 | "--mount", "type=volume,source="+volumeID+",target=/work", |
341 | | - "--mount", "type=bind,source="+archiveWithAdditionalFiles.mockAdditionalFilePaths[".gitignore"]+",target=/tmp/src-additional-file-0,ro", |
342 | | - "--mount", "type=bind,source="+archiveWithAdditionalFiles.mockAdditionalFilePaths["another-file"]+",target=/tmp/src-additional-file-1,ro", |
343 | 341 | DockerVolumeWorkspaceImage, |
344 | | - "sh", "-c", `while test "$#" -gt 0; do cp "$1" "$2" || exit; shift 2; done`, |
345 | | - "copy-additional-files", |
346 | | - "/tmp/src-additional-file-0", "/work/.gitignore", |
347 | | - "/tmp/src-additional-file-1", "/work/another-file", |
| 342 | + "tar", "-xf", "/tmp/additional-files.tar", "-C", "/work", |
348 | 343 | ), |
349 | 344 | expect.NewGlob( |
350 | 345 | expect.Success, |
@@ -388,53 +383,54 @@ func TestVolumeWorkspaceCreator(t *testing.T) { |
388 | 383 | } |
389 | 384 | } |
390 | 385 |
|
391 | | -func TestCopyFilesIntoVolumesDoesNotInterpolateNames(t *testing.T) { |
392 | | - const maliciousName = "x,ro,type=bind,source=/var/run/docker.sock,target=/h1sock;touch /work/injected;/.gitignore" |
| 386 | +func TestArchiveAdditionalFilesTreatsRepositoryPathsAsData(t *testing.T) { |
| 387 | + const maliciousName = "x;touch${IFS}/tmp/pwned,source=.,target=/x/.gitignore" |
| 388 | + source := filepath.Join(t.TempDir(), "additional-file") |
| 389 | + if err := os.WriteFile(source, []byte("contents"), 0600); err != nil { |
| 390 | + t.Fatal(err) |
| 391 | + } |
393 | 392 |
|
394 | | - expect.Commands( |
395 | | - t, |
396 | | - expect.NewGlob( |
397 | | - expect.Success, |
398 | | - "docker", "run", "--rm", "--init", "--workdir", "/work", |
399 | | - "--user", "0:0", |
400 | | - "--mount", "type=volume,source="+volumeID+",target=/work", |
401 | | - "--mount", "type=bind,source=/tmp/additional-file,target=/tmp/src-additional-file-0,ro", |
402 | | - DockerVolumeWorkspaceImage, |
403 | | - "sh", "-c", `while test "$#" -gt 0; do cp "$1" "$2" || exit; shift 2; done`, |
404 | | - "copy-additional-files", |
405 | | - "/tmp/src-additional-file-0", "/work/"+maliciousName, |
406 | | - ), |
407 | | - ) |
| 393 | + wc := &dockerVolumeWorkspaceCreator{tempDir: t.TempDir()} |
| 394 | + archive, err := wc.archiveAdditionalFiles(map[string]string{maliciousName: source}) |
| 395 | + if err != nil { |
| 396 | + t.Fatal(err) |
| 397 | + } |
| 398 | + defer os.Remove(archive) |
408 | 399 |
|
409 | | - wc := &dockerVolumeWorkspaceCreator{} |
410 | | - w := &dockerVolumeWorkspace{volume: volumeID} |
411 | | - err := wc.copyFilesIntoVolumes(context.Background(), w, map[string]string{ |
412 | | - maliciousName: "/tmp/additional-file", |
413 | | - }) |
| 400 | + f, err := os.Open(archive) |
414 | 401 | if err != nil { |
415 | | - t.Fatalf("unexpected error: %v", err) |
| 402 | + t.Fatal(err) |
416 | 403 | } |
417 | | -} |
| 404 | + defer f.Close() |
418 | 405 |
|
419 | | -func TestCopyFilesIntoVolumesRejectsUnsafePaths(t *testing.T) { |
420 | | - tests := map[string]map[string]string{ |
421 | | - "workspace traversal": { |
422 | | - "../etc/.gitignore": "/tmp/additional-file", |
423 | | - }, |
424 | | - "mount source injection": { |
425 | | - ".gitignore": "/tmp/additional-file,source=/etc", |
426 | | - }, |
| 406 | + r := tar.NewReader(f) |
| 407 | + header, err := r.Next() |
| 408 | + if err != nil { |
| 409 | + t.Fatal(err) |
| 410 | + } |
| 411 | + if header.Name != maliciousName { |
| 412 | + t.Fatalf("unexpected archived path: have=%q want=%q", header.Name, maliciousName) |
| 413 | + } |
| 414 | + contents, err := io.ReadAll(r) |
| 415 | + if err != nil { |
| 416 | + t.Fatal(err) |
427 | 417 | } |
| 418 | + if string(contents) != "contents" { |
| 419 | + t.Fatalf("unexpected archived contents: %q", contents) |
| 420 | + } |
| 421 | + if _, err := r.Next(); err != io.EOF { |
| 422 | + t.Fatalf("unexpected second archive entry: %v", err) |
| 423 | + } |
| 424 | +} |
428 | 425 |
|
429 | | - for name, files := range tests { |
430 | | - t.Run(name, func(t *testing.T) { |
431 | | - expect.Commands(t) |
432 | | - wc := &dockerVolumeWorkspaceCreator{} |
433 | | - w := &dockerVolumeWorkspace{volume: volumeID} |
434 | | - if err := wc.copyFilesIntoVolumes(context.Background(), w, files); err == nil { |
435 | | - t.Fatal("expected unsafe path to be rejected") |
436 | | - } |
437 | | - }) |
| 426 | +func TestCopyFilesIntoVolumesRejectsWorkspaceTraversal(t *testing.T) { |
| 427 | + expect.Commands(t) |
| 428 | + wc := &dockerVolumeWorkspaceCreator{} |
| 429 | + w := &dockerVolumeWorkspace{volume: volumeID} |
| 430 | + if err := wc.copyFilesIntoVolumes(context.Background(), w, map[string]string{ |
| 431 | + "../etc/.gitignore": "/tmp/additional-file", |
| 432 | + }); err == nil { |
| 433 | + t.Fatal("expected workspace traversal to be rejected") |
438 | 434 | } |
439 | 435 | } |
440 | 436 |
|
|
0 commit comments