From 865620bad3d2e7f98242e00bf475e8ec4062985c Mon Sep 17 00:00:00 2001 From: Jan Provaznik <jprovaznik@gitlab.com> Date: Wed, 16 Jan 2019 19:29:57 +0100 Subject: [PATCH] Remove EXIF from JPEG/TIFF images EXIF may contain sensitive information, when uploading any file which may be an image (based on filename suffix), we run it through exiftool which removes any metadata. --- .gitlab-ci.yml | 1 + README.md | 19 ++++ VERSION | 2 +- internal/helper/helpers.go | 6 +- internal/upload/exif/exif.go | 104 ++++++++++++++++++ internal/upload/exif/exif_test.go | 77 +++++++++++++ internal/upload/exif/testdata/sample_exif.jpg | Bin 0 -> 33881 bytes internal/upload/rewrite.go | 26 ++++- internal/upload/uploads.go | 3 + internal/upload/uploads_test.go | 90 +++++++++++++++ 10 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 internal/upload/exif/exif.go create mode 100644 internal/upload/exif/exif_test.go create mode 100644 internal/upload/exif/testdata/sample_exif.jpg diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a190ea429d1f..78444f7a80cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,7 @@ verify: GITALY_ADDRESS: "tcp://gitaly:8075" script: - go version + - apt-get update && apt-get -y install libimage-exiftool-perl - make test test using go 1.10: diff --git a/README.md b/README.md index 5dfce77d5df9..7e94ae6ec793 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,25 @@ make install PREFIX=/foo On some operating systems, such as FreeBSD, you may have to use `gmake` instead of `make`. +## Dependencies + +### Exiftool + +Workhorse uses [exiftool](https://www.sno.phy.queensu.ca/~phil/exiftool/) for +removing EXIF data (which may contain sensitive information) from uploaded +images. If you installed GitLab: + +- Using the Omnibus package, you're all set. +- From source, make sure `exiftool` is installed: + + ```sh + # Debian/Ubuntu + sudo apt-get install libimage-exiftool-perl + + # RHEL/CentOS + sudo yum install perl-Image-ExifTool + ``` + ## Error tracking GitLab-Workhorse supports remote error tracking with diff --git a/VERSION b/VERSION index 2bf50aaf17a6..9c78b761ea12 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.0 +8.3.2 diff --git a/internal/helper/helpers.go b/internal/helper/helpers.go index 5d2cb280871d..2acc357b278f 100644 --- a/internal/helper/helpers.go +++ b/internal/helper/helpers.go @@ -37,7 +37,11 @@ func LogError(r *http.Request, err error) { } func RequestEntityTooLarge(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, "Request Entity Too Large", http.StatusRequestEntityTooLarge) + CaptureAndFail(w, r, err, "Request Entity Too Large", http.StatusRequestEntityTooLarge) +} + +func CaptureAndFail(w http.ResponseWriter, r *http.Request, err error, msg string, code int) { + http.Error(w, msg, code) captureRavenError(r, err) printError(r, err) } diff --git a/internal/upload/exif/exif.go b/internal/upload/exif/exif.go new file mode 100644 index 000000000000..bf25e595deb5 --- /dev/null +++ b/internal/upload/exif/exif.go @@ -0,0 +1,104 @@ +package exif + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os/exec" + "regexp" + + "gitlab.com/gitlab-org/gitlab-workhorse/internal/log" +) + +var ErrRemovingExif = errors.New("error while removing EXIF") + +type cleaner struct { + ctx context.Context + cmd *exec.Cmd + stdout io.Reader + stderr bytes.Buffer + waitDone chan struct{} + waitErr error +} + +func NewCleaner(ctx context.Context, stdin io.Reader) (io.Reader, error) { + c := &cleaner{ + ctx: ctx, + waitDone: make(chan struct{}), + } + + if err := c.startProcessing(stdin); err != nil { + return nil, err + } + + return c, nil +} + +func (c *cleaner) Read(p []byte) (int, error) { + n, err := c.stdout.Read(p) + if err == io.EOF { + if waitErr := c.wait(); waitErr != nil { + log.WithFields(c.ctx, log.Fields{ + "command": c.cmd.Args, + "stderr": c.stderr.String(), + "error": waitErr.Error(), + }).Print("exiftool command failed") + return n, ErrRemovingExif + } + } + + return n, err +} + +func (c *cleaner) startProcessing(stdin io.Reader) error { + var err error + + whitelisted_tags := []string{ + "-ResolutionUnit", + "-XResolution", + "-YResolution", + "-YCbCrSubSampling", + "-YCbCrPositioning", + "-BitsPerSample", + "-ImageHeight", + "-ImageWidth", + "-ImageSize", + "-Copyright", + "-CopyrightNotice", + } + + args := append([]string{"-all=", "--IPTC:all", "--XMP-iptcExt:all", "-tagsFromFile", "@"}, whitelisted_tags...) + args = append(args, "-") + c.cmd = exec.CommandContext(c.ctx, "exiftool", args...) + + c.cmd.Stderr = &c.stderr + c.cmd.Stdin = stdin + + c.stdout, err = c.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %v", err) + } + + if err = c.cmd.Start(); err != nil { + return fmt.Errorf("start %v: %v", c.cmd.Args, err) + } + go func() { + c.waitErr = c.cmd.Wait() + close(c.waitDone) + }() + + return nil +} + +func (c *cleaner) wait() error { + <-c.waitDone + return c.waitErr +} + +func IsExifFile(filename string) bool { + filenameMatch := regexp.MustCompile(`(?i)\.(jpg|jpeg|tiff)$`) + + return filenameMatch.MatchString(filename) +} diff --git a/internal/upload/exif/exif_test.go b/internal/upload/exif/exif_test.go new file mode 100644 index 000000000000..83a3d7edb093 --- /dev/null +++ b/internal/upload/exif/exif_test.go @@ -0,0 +1,77 @@ +package exif + +import ( + "context" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsExifFile(t *testing.T) { + tests := []struct { + name string + expected bool + }{ + { + name: "/full/path.jpg", + expected: true, + }, + { + name: "path.jpeg", + expected: true, + }, + { + name: "path.tiff", + expected: true, + }, + { + name: "path.JPG", + expected: true, + }, + { + name: "path.tar", + expected: false, + }, + { + name: "path", + expected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.expected, IsExifFile(test.name)) + }) + } +} + +func TestNewCleanerWithValidFile(t *testing.T) { + input, err := os.Open("testdata/sample_exif.jpg") + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cleaner, err := NewCleaner(ctx, input) + require.NoError(t, err, "Expected no error when creating cleaner command") + + size, err := io.Copy(ioutil.Discard, cleaner) + require.NoError(t, err, "Expected no error when reading output") + + sizeAfterStrip := int64(25399) + require.Equal(t, sizeAfterStrip, size, "Different size of converted image") +} + +func TestNewCleanerWithInvalidFile(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cleaner, err := NewCleaner(ctx, strings.NewReader("invalid image")) + require.NoError(t, err, "Expected no error when creating cleaner command") + + size, err := io.Copy(ioutil.Discard, cleaner) + require.Error(t, err, "Expected error when reading output") + require.Equal(t, int64(0), size, "Size of invalid image should be 0") +} diff --git a/internal/upload/exif/testdata/sample_exif.jpg b/internal/upload/exif/testdata/sample_exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..05eda3f7f953eec3f7f9c08428328d9c3922d7d9 GIT binary patch literal 33881 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 F69C`65P1Lq literal 0 HcmV?d00001 diff --git a/internal/upload/rewrite.go b/internal/upload/rewrite.go index 2086c8e95e7e..d7c4d777eab3 100644 --- a/internal/upload/rewrite.go +++ b/internal/upload/rewrite.go @@ -12,6 +12,8 @@ import ( "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/log" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/upload/exif" ) var ( @@ -113,12 +115,30 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa opts := filestore.GetOpts(rew.preauth) opts.TempFilePrefix = filename - fh, err := filestore.SaveFileFromReader(ctx, p, -1, opts) + var inputReader io.Reader + if exif.IsExifFile(filename) { + log.WithFields(ctx, log.Fields{ + "filename": filename, + }).Print("running exiftool to remove any metadata") + + cleaner, err := exif.NewCleaner(ctx, p) + if err != nil { + return fmt.Errorf("failed to start EXIF metadata cleaner: %v", err) + } + + inputReader = cleaner + } else { + inputReader = p + } + + fh, err := filestore.SaveFileFromReader(ctx, inputReader, -1, opts) if err != nil { - if err == filestore.ErrEntityTooLarge { + switch err { + case filestore.ErrEntityTooLarge, exif.ErrRemovingExif: return err + default: + return fmt.Errorf("persisting multipart file: %v", err) } - return fmt.Errorf("persisting multipart file: %v", err) } for key, value := range fh.GitLabFinalizeFields(name) { diff --git a/internal/upload/uploads.go b/internal/upload/uploads.go index 766d5552096b..32bf514ee1fb 100644 --- a/internal/upload/uploads.go +++ b/internal/upload/uploads.go @@ -11,6 +11,7 @@ import ( "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/upload/exif" ) // These methods are allowed to have thread-unsafe implementations. @@ -40,6 +41,8 @@ func HandleFileUploads(w http.ResponseWriter, r *http.Request, h http.Handler, p h.ServeHTTP(w, r) case filestore.ErrEntityTooLarge: helper.RequestEntityTooLarge(w, r, err) + case exif.ErrRemovingExif: + helper.CaptureAndFail(w, r, err, "Failed to process image", http.StatusUnprocessableEntity) default: helper.Fail500(w, r, fmt.Errorf("handleFileUploads: extract files from multipart: %v", err)) } diff --git a/internal/upload/uploads_test.go b/internal/upload/uploads_test.go index 45f0f6e0a501..e974569fa1b1 100644 --- a/internal/upload/uploads_test.go +++ b/internal/upload/uploads_test.go @@ -12,10 +12,13 @@ import ( "net/http/httptest" "os" "regexp" + "strconv" "strings" "testing" "time" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" @@ -323,6 +326,93 @@ func TestInvalidFileNames(t *testing.T) { } } +func TestUploadHandlerRemovingExif(t *testing.T) { + tempPath, err := ioutil.TempDir("", "uploads") + require.NoError(t, err) + defer os.RemoveAll(tempPath) + + var buffer bytes.Buffer + + content, err := ioutil.ReadFile("exif/testdata/sample_exif.jpg") + require.NoError(t, err) + + writer := multipart.NewWriter(&buffer) + file, err := writer.CreateFormFile("file", "test.jpg") + require.NoError(t, err) + + _, err = file.Write(content) + require.NoError(t, err) + + err = writer.Close() + require.NoError(t, err) + + ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(100000) + require.NoError(t, err) + + size, err := strconv.Atoi(r.FormValue("file.size")) + require.NoError(t, err) + require.True(t, size < len(content), "Expected the file to be smaller after removal of exif") + require.True(t, size > 0, "Expected to receive not empty file") + + w.WriteHeader(200) + fmt.Fprint(w, "RESPONSE") + }) + defer ts.Close() + + httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", &buffer) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + httpRequest = httpRequest.WithContext(ctx) + httpRequest.ContentLength = int64(buffer.Len()) + httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) + response := httptest.NewRecorder() + + handler := newProxy(ts.URL) + HandleFileUploads(response, httpRequest, handler, &api.Response{TempPath: tempPath}, &testFormProcessor{}) + testhelper.AssertResponseCode(t, response, 200) +} + +func TestUploadHandlerRemovingInvalidExif(t *testing.T) { + tempPath, err := ioutil.TempDir("", "uploads") + require.NoError(t, err) + defer os.RemoveAll(tempPath) + + var buffer bytes.Buffer + + writer := multipart.NewWriter(&buffer) + file, err := writer.CreateFormFile("file", "test.jpg") + require.NoError(t, err) + + fmt.Fprint(file, "this is not valid image data") + err = writer.Close() + require.NoError(t, err) + + ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(100000) + require.Error(t, err) + }) + defer ts.Close() + + httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", &buffer) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + httpRequest = httpRequest.WithContext(ctx) + httpRequest.ContentLength = int64(buffer.Len()) + httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) + response := httptest.NewRecorder() + + handler := newProxy(ts.URL) + HandleFileUploads(response, httpRequest, handler, &api.Response{TempPath: tempPath}, &testFormProcessor{}) + testhelper.AssertResponseCode(t, response, 422) +} + func newProxy(url string) *proxy.Proxy { parsedURL := helper.URLMustParse(url) return proxy.NewProxy(parsedURL, "123", roundtripper.NewTestBackendRoundTripper(parsedURL)) -- GitLab