From 7667ad88c5489206cf334524045ba38cc5029bd8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 29 Mar 2018 19:22:19 +0200 Subject: [PATCH 1/2] Add zip file extractor --- internal/deploy/extractzip.go | 75 +++++++++++++++++++++++++++++ internal/deploy/extractzip_test.go | 63 ++++++++++++++++++++++++ internal/deploy/testdata/test1.zip | Bin 0 -> 1076 bytes internal/deploy/testdata/test2.zip | Bin 0 -> 160 bytes 4 files changed, 138 insertions(+) create mode 100644 internal/deploy/extractzip.go create mode 100644 internal/deploy/extractzip_test.go create mode 100644 internal/deploy/testdata/test1.zip create mode 100644 internal/deploy/testdata/test2.zip diff --git a/internal/deploy/extractzip.go b/internal/deploy/extractzip.go new file mode 100644 index 000000000..e024456a7 --- /dev/null +++ b/internal/deploy/extractzip.go @@ -0,0 +1,75 @@ +package deploy + +import ( + "archive/zip" + "errors" + "io" + "os" + "path" + "path/filepath" + "strings" +) + +const extractPrefix = "public/" + +var ErrNoPublicFiles = errors.New("error: archive has no files in public/") + +func ExtractZip(archivePath string, extractPath string) error { + archive, err := zip.OpenReader(archivePath) + if err != nil { + return err + } + defer archive.Close() + + extracted := 0 + for _, f := range archive.File { + fi := f.FileInfo() + if fi.IsDir() { + continue + } + + cleanName := filepath.Clean(f.Name) + if !strings.HasPrefix(cleanName, extractPrefix) { + continue + } + + if err := extractZipFile(f, path.Join(extractPath, cleanName)); err != nil { + return err + } + + extracted++ + } + + if extracted == 0 { + return ErrNoPublicFiles + } + + return nil +} + +func extractZipFile(f *zip.File, extractPath string) error { + dir := filepath.Dir(extractPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + // TODO handle symlinks + + r, err := f.Open() + if err != nil { + return err + } + defer r.Close() + + w, err := os.OpenFile(extractPath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer w.Close() + + if _, err := io.Copy(w, r); err != nil { + return err + } + + return w.Close() +} diff --git a/internal/deploy/extractzip_test.go b/internal/deploy/extractzip_test.go new file mode 100644 index 000000000..2f15ff038 --- /dev/null +++ b/internal/deploy/extractzip_test.go @@ -0,0 +1,63 @@ +package deploy + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractZip(t *testing.T) { + testCases := []struct { + desc string + archive string + mustExist map[string]string + mustNotExist []string + matchError func(error) bool + }{ + { + desc: "archive with file inside and outside public/", + archive: "testdata/test1.zip", + mustExist: map[string]string{ + "public/h/e/l/l/o": "world\n", + }, + mustNotExist: []string{"foo"}, + }, + { + desc: "archive with no file in public/", + archive: "testdata/test2.zip", + matchError: func(err error) bool { + return err == ErrNoPublicFiles + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "gitlab-pages") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + extractError := ExtractZip(tc.archive, tmpDir) + if tc.matchError != nil { + require.True(t, tc.matchError(extractError), "error %v does not match", extractError) + return + } + + require.NoError(t, extractError) + + for file, content := range tc.mustExist { + actualContent, err := ioutil.ReadFile(path.Join(tmpDir, file)) + require.NoError(t, err, "read %q", file) + require.Equal(t, content, string(actualContent), "content of %q", file) + } + + for _, file := range tc.mustNotExist { + _, err = os.Stat(path.Join(tmpDir, file)) + require.True(t, os.IsNotExist(err), "error should be '%q does not exist': %v", file, err) + } + }) + } +} diff --git a/internal/deploy/testdata/test1.zip b/internal/deploy/testdata/test1.zip new file mode 100644 index 0000000000000000000000000000000000000000..10a3258f945890e6cd6e6c5c4cf52b52c9d9a4f9 GIT binary patch literal 1076 zcmWIWW@h1H0D+3hwLUL5FJfU~U|;}YW(FCCwEX+#>l|Hu%r>h1jjrpB<9g#9oZnvIENU-z#zcz*3prH z0g(b&8JIyS0FPmq@sDg+6T&c1YG7qx2BiiPjN@iNHts00aiEkzx`DhX268dNJ)5DQ zst<}>!bXCk78Kkt%#UJZAi9xILqYM3$KxPFQPT^=$ Date: Thu, 29 Mar 2018 19:33:55 +0200 Subject: [PATCH 2/2] Capture status quo in tests --- internal/deploy/extractzip_test.go | 18 ++++++++++++++++++ internal/deploy/testdata/.gitignore | 1 + internal/deploy/testdata/test3.zip | Bin 0 -> 636 bytes 3 files changed, 19 insertions(+) create mode 100644 internal/deploy/testdata/.gitignore create mode 100644 internal/deploy/testdata/test3.zip diff --git a/internal/deploy/extractzip_test.go b/internal/deploy/extractzip_test.go index 2f15ff038..dd27050b1 100644 --- a/internal/deploy/extractzip_test.go +++ b/internal/deploy/extractzip_test.go @@ -32,6 +32,24 @@ func TestExtractZip(t *testing.T) { return err == ErrNoPublicFiles }, }, + { + desc: "archive with evil symlink", + archive: "testdata/test3.zip", + mustExist: map[string]string{ + // The test3.zip archive contains a symlink to /etc/passwd. This test + // asserts that instead of that symlink, we get a regular file whose + // contents are "/etc/passwd". If the extracted "public/passwd" was an + // actual symlink we would get the contents of the /etc/passwd file of + // the system where the test runs. + "public/passwd": "/etc/passwd", + // The "public/bar" symlink tries to point to "foo" but we don't support + // symlinks at the moment. Instead it creates a regular file with + // contents "bar". TODO: support valid symlinks? + "public/bar": "foo", + // "foo" is a regular file with contents "not-bar" + "public/foo": "not-bar\n", + }, + }, } for _, tc := range testCases { diff --git a/internal/deploy/testdata/.gitignore b/internal/deploy/testdata/.gitignore new file mode 100644 index 000000000..c75eeccc8 --- /dev/null +++ b/internal/deploy/testdata/.gitignore @@ -0,0 +1 @@ +/public diff --git a/internal/deploy/testdata/test3.zip b/internal/deploy/testdata/test3.zip new file mode 100644 index 0000000000000000000000000000000000000000..1af5da34ab1338885d12c862bed82d2ba573f668 GIT binary patch literal 636 zcmWIWW@h1H0D<1wwLTykhS?cp7z#?0ax#{_4dnL7hH7#J8pmCM6beAlA>hij+2CN~2E0|@gXoK%olTwI<4 zHc3-yZxjflnWUdulB^F=7U0duB*%;^oFt$@CcyC45yXUt8Y?8!(9A_P3^U9ihA}cQ zFf3^VnFcis9D=M2%%Bj&V;*L>LCga=>+Ql`1_oG^f