utils_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package archive // import "github.com/docker/docker/pkg/archive"
  2. import (
  3. "archive/tar"
  4. "bytes"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "time"
  10. )
  11. var testUntarFns = map[string]func(string, io.Reader) error{
  12. "untar": func(dest string, r io.Reader) error {
  13. return Untar(r, dest, nil)
  14. },
  15. "applylayer": func(dest string, r io.Reader) error {
  16. _, err := ApplyLayer(dest, r)
  17. return err
  18. },
  19. }
  20. // testBreakout is a helper function that, within the provided `tmpdir` directory,
  21. // creates a `victim` folder with a generated `hello` file in it.
  22. // `untar` extracts to a directory named `dest`, the tar file created from `headers`.
  23. //
  24. // Here are the tested scenarios:
  25. // - removed `victim` folder (write)
  26. // - removed files from `victim` folder (write)
  27. // - new files in `victim` folder (write)
  28. // - modified files in `victim` folder (write)
  29. // - file in `dest` with same content as `victim/hello` (read)
  30. //
  31. // When using testBreakout make sure you cover one of the scenarios listed above.
  32. func testBreakout(untarFn string, tmpdir string, headers []*tar.Header) error {
  33. tmpdir, err := os.MkdirTemp("", tmpdir)
  34. if err != nil {
  35. return err
  36. }
  37. defer os.RemoveAll(tmpdir)
  38. dest := filepath.Join(tmpdir, "dest")
  39. if err := os.Mkdir(dest, 0o755); err != nil {
  40. return err
  41. }
  42. victim := filepath.Join(tmpdir, "victim")
  43. if err := os.Mkdir(victim, 0o755); err != nil {
  44. return err
  45. }
  46. hello := filepath.Join(victim, "hello")
  47. helloData, err := time.Now().MarshalText()
  48. if err != nil {
  49. return err
  50. }
  51. if err := os.WriteFile(hello, helloData, 0o644); err != nil {
  52. return err
  53. }
  54. helloStat, err := os.Stat(hello)
  55. if err != nil {
  56. return err
  57. }
  58. reader, writer := io.Pipe()
  59. go func() {
  60. t := tar.NewWriter(writer)
  61. for _, hdr := range headers {
  62. t.WriteHeader(hdr)
  63. }
  64. t.Close()
  65. }()
  66. untar := testUntarFns[untarFn]
  67. if untar == nil {
  68. return fmt.Errorf("could not find untar function %q in testUntarFns", untarFn)
  69. }
  70. if err := untar(dest, reader); err != nil {
  71. if _, ok := err.(breakoutError); !ok {
  72. // If untar returns an error unrelated to an archive breakout,
  73. // then consider this an unexpected error and abort.
  74. return err
  75. }
  76. // Here, untar detected the breakout.
  77. // Let's move on verifying that indeed there was no breakout.
  78. fmt.Printf("breakoutError: %v\n", err)
  79. }
  80. // Check victim folder
  81. f, err := os.Open(victim)
  82. if err != nil {
  83. // codepath taken if victim folder was removed
  84. return fmt.Errorf("archive breakout: error reading %q: %v", victim, err)
  85. }
  86. defer f.Close()
  87. // Check contents of victim folder
  88. //
  89. // We are only interested in getting 2 files from the victim folder, because if all is well
  90. // we expect only one result, the `hello` file. If there is a second result, it cannot
  91. // hold the same name `hello` and we assume that a new file got created in the victim folder.
  92. // That is enough to detect an archive breakout.
  93. names, err := f.Readdirnames(2)
  94. if err != nil {
  95. // codepath taken if victim is not a folder
  96. return fmt.Errorf("archive breakout: error reading directory content of %q: %v", victim, err)
  97. }
  98. for _, name := range names {
  99. if name != "hello" {
  100. // codepath taken if new file was created in victim folder
  101. return fmt.Errorf("archive breakout: new file %q", name)
  102. }
  103. }
  104. // Check victim/hello
  105. f, err = os.Open(hello)
  106. if err != nil {
  107. // codepath taken if read permissions were removed
  108. return fmt.Errorf("archive breakout: could not lstat %q: %v", hello, err)
  109. }
  110. defer f.Close()
  111. b, err := io.ReadAll(f)
  112. if err != nil {
  113. return err
  114. }
  115. fi, err := f.Stat()
  116. if err != nil {
  117. return err
  118. }
  119. if helloStat.IsDir() != fi.IsDir() ||
  120. // TODO: cannot check for fi.ModTime() change
  121. helloStat.Mode() != fi.Mode() ||
  122. helloStat.Size() != fi.Size() ||
  123. !bytes.Equal(helloData, b) {
  124. // codepath taken if hello has been modified
  125. return fmt.Errorf("archive breakout: file %q has been modified. Contents: expected=%q, got=%q. FileInfo: expected=%#v, got=%#v", hello, helloData, b, helloStat, fi)
  126. }
  127. // Check that nothing in dest/ has the same content as victim/hello.
  128. // Since victim/hello was generated with time.Now(), it is safe to assume
  129. // that any file whose content matches exactly victim/hello, managed somehow
  130. // to access victim/hello.
  131. return filepath.WalkDir(dest, func(path string, info os.DirEntry, err error) error {
  132. if info.IsDir() {
  133. if err != nil {
  134. // skip directory if error
  135. return filepath.SkipDir
  136. }
  137. // enter directory
  138. return nil
  139. }
  140. if err != nil {
  141. // skip file if error
  142. return nil
  143. }
  144. b, err := os.ReadFile(path)
  145. if err != nil {
  146. // Houston, we have a problem. Aborting (space)walk.
  147. return err
  148. }
  149. if bytes.Equal(helloData, b) {
  150. return fmt.Errorf("archive breakout: file %q has been accessed via %q", hello, path)
  151. }
  152. return nil
  153. })
  154. }