Commit 84c6b352 authored by Jan Provaznik's avatar Jan Provaznik Committed by Yorick Peterse

Check content type before running exiftool

Assures that exiftool runs for jpeg/tiff images only.
parent 1a4235b3
---
title: Check image content type before running exiftool in workhorse
merge_request:
author:
type: security
...@@ -31,6 +31,7 @@ require ( ...@@ -31,6 +31,7 @@ require (
gitlab.com/gitlab-org/gitaly v1.74.0 gitlab.com/gitlab-org/gitaly v1.74.0
gitlab.com/gitlab-org/labkit v1.0.0 gitlab.com/gitlab-org/labkit v1.0.0
gocloud.dev v0.21.1-0.20201223184910-5094f54ed8bb gocloud.dev v0.21.1-0.20201223184910-5094f54ed8bb
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
golang.org/x/lint v0.0.0-20200302205851-738671d3881b golang.org/x/lint v0.0.0-20200302205851-738671d3881b
golang.org/x/net v0.0.0-20201224014010-6772e930b67b golang.org/x/net v0.0.0-20201224014010-6772e930b67b
golang.org/x/text v0.3.5 // indirect golang.org/x/text v0.3.5 // indirect
......
...@@ -23,6 +23,14 @@ type cleaner struct { ...@@ -23,6 +23,14 @@ type cleaner struct {
eof bool eof bool
} }
type FileType int
const (
TypeUnknown FileType = iota
TypeJPEG
TypeTIFF
)
func NewCleaner(ctx context.Context, stdin io.Reader) (io.ReadCloser, error) { func NewCleaner(ctx context.Context, stdin io.Reader) (io.ReadCloser, error) {
c := &cleaner{ctx: ctx} c := &cleaner{ctx: ctx}
...@@ -101,11 +109,20 @@ func (c *cleaner) startProcessing(stdin io.Reader) error { ...@@ -101,11 +109,20 @@ func (c *cleaner) startProcessing(stdin io.Reader) error {
return nil return nil
} }
func IsExifFile(filename string) bool { func FileTypeFromSuffix(filename string) FileType {
if os.Getenv("SKIP_EXIFTOOL") == "1" { if os.Getenv("SKIP_EXIFTOOL") == "1" {
return false return TypeUnknown
}
jpegMatch := regexp.MustCompile(`(?i)^[^\n]*\.(jpg|jpeg)$`)
if jpegMatch.MatchString(filename) {
return TypeJPEG
}
tiffMatch := regexp.MustCompile(`(?i)^[^\n]*\.tiff$`)
if tiffMatch.MatchString(filename) {
return TypeTIFF
} }
filenameMatch := regexp.MustCompile(`(?i)\.(jpg|jpeg|tiff)$`)
return filenameMatch.MatchString(filename) return TypeUnknown
} }
...@@ -11,39 +11,57 @@ import ( ...@@ -11,39 +11,57 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestIsExifFile(t *testing.T) { func TestFileTypeFromSuffix(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
expected bool expected FileType
}{ }{
{ {
name: "/full/path.jpg", name: "/full/path.jpg",
expected: true, expected: TypeJPEG,
}, },
{ {
name: "path.jpeg", name: "path.jpeg",
expected: true, expected: TypeJPEG,
}, },
{ {
name: "path.tiff", name: "path.tiff",
expected: true, expected: TypeTIFF,
}, },
{ {
name: "path.JPG", name: "path.JPG",
expected: true, expected: TypeJPEG,
}, },
{ {
name: "path.tar", name: "path.tar",
expected: false, expected: TypeUnknown,
}, },
{ {
name: "path", name: "path",
expected: false, expected: TypeUnknown,
},
{
name: "something.jpg.py",
expected: TypeUnknown,
},
{
name: "something.py.jpg",
expected: TypeJPEG,
},
{
name: `something.jpg
.py`,
expected: TypeUnknown,
},
{
name: `something.something
.jpg`,
expected: TypeUnknown,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.expected, IsExifFile(test.name)) require.Equal(t, test.expected, FileTypeFromSuffix(test.name))
}) })
} }
} }
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
...@@ -15,6 +16,8 @@ import ( ...@@ -15,6 +16,8 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"gitlab.com/gitlab-org/labkit/log" "gitlab.com/gitlab-org/labkit/log"
"golang.org/x/image/tiff"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api" "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore" "gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/lsif_transformer/parser" "gitlab.com/gitlab-org/gitlab-workhorse/internal/lsif_transformer/parser"
...@@ -127,9 +130,11 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa ...@@ -127,9 +130,11 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa
var inputReader io.ReadCloser var inputReader io.ReadCloser
var err error var err error
imageType := exif.FileTypeFromSuffix(filename)
switch { switch {
case exif.IsExifFile(filename): case imageType != exif.TypeUnknown:
inputReader, err = handleExifUpload(ctx, p, filename) inputReader, err = handleExifUpload(ctx, p, filename, imageType)
if err != nil { if err != nil {
return err return err
} }
...@@ -169,12 +174,48 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa ...@@ -169,12 +174,48 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa
return rew.filter.ProcessFile(ctx, name, fh, rew.writer) return rew.filter.ProcessFile(ctx, name, fh, rew.writer)
} }
func handleExifUpload(ctx context.Context, r io.Reader, filename string) (io.ReadCloser, error) { func handleExifUpload(ctx context.Context, r io.Reader, filename string, imageType exif.FileType) (io.ReadCloser, error) {
tmpfile, err := ioutil.TempFile("", "exifremove")
if err != nil {
return nil, err
}
go func() {
<-ctx.Done()
tmpfile.Close()
}()
if err := os.Remove(tmpfile.Name()); err != nil {
return nil, err
}
_, err = io.Copy(tmpfile, r)
if err != nil {
return nil, err
}
tmpfile.Seek(0, io.SeekStart)
isValidType := false
switch imageType {
case exif.TypeJPEG:
isValidType = isJPEG(tmpfile)
case exif.TypeTIFF:
isValidType = isTIFF(tmpfile)
}
tmpfile.Seek(0, io.SeekStart)
if !isValidType {
log.WithContextFields(ctx, log.Fields{
"filename": filename,
"imageType": imageType,
}).Print("invalid content type, not running exiftool")
return tmpfile, nil
}
log.WithContextFields(ctx, log.Fields{ log.WithContextFields(ctx, log.Fields{
"filename": filename, "filename": filename,
}).Print("running exiftool to remove any metadata") }).Print("running exiftool to remove any metadata")
cleaner, err := exif.NewCleaner(ctx, r) cleaner, err := exif.NewCleaner(ctx, tmpfile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -182,6 +223,29 @@ func handleExifUpload(ctx context.Context, r io.Reader, filename string) (io.Rea ...@@ -182,6 +223,29 @@ func handleExifUpload(ctx context.Context, r io.Reader, filename string) (io.Rea
return cleaner, nil return cleaner, nil
} }
func isTIFF(r io.Reader) bool {
_, err := tiff.Decode(r)
if err == nil {
return true
}
if _, unsupported := err.(tiff.UnsupportedError); unsupported {
return true
}
return false
}
func isJPEG(r io.Reader) bool {
// Only the first 512 bytes are used to sniff the content type.
buf, err := ioutil.ReadAll(io.LimitReader(r, 512))
if err != nil {
return false
}
return http.DetectContentType(buf) == "image/jpeg"
}
func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string, preauth *api.Response) (io.ReadCloser, error) { func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string, preauth *api.Response) (io.ReadCloser, error) {
parserConfig := parser.Config{ parserConfig := parser.Config{
TempPath: tempPath, TempPath: tempPath,
......
package upload
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestImageTypeRecongition(t *testing.T) {
tests := []struct {
filename string
isJPEG bool
isTIFF bool
}{
{
filename: "exif/testdata/sample_exif.jpg",
isJPEG: true,
isTIFF: false,
}, {
filename: "exif/testdata/sample_exif.tiff",
isJPEG: false,
isTIFF: true,
}, {
filename: "exif/testdata/sample_exif_corrupted.jpg",
isJPEG: true,
isTIFF: false,
}, {
filename: "exif/testdata/sample_exif_invalid.jpg",
isJPEG: false,
isTIFF: false,
},
}
for _, test := range tests {
t.Run(test.filename, func(t *testing.T) {
input, err := os.Open(test.filename)
require.NoError(t, err)
require.Equal(t, test.isJPEG, isJPEG(input))
require.Equal(t, test.isTIFF, isTIFF(input))
})
}
}
...@@ -366,26 +366,28 @@ func TestInvalidFileNames(t *testing.T) { ...@@ -366,26 +366,28 @@ func TestInvalidFileNames(t *testing.T) {
} }
func TestUploadHandlerRemovingExif(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") content, err := ioutil.ReadFile("exif/testdata/sample_exif.jpg")
require.NoError(t, err) require.NoError(t, err)
writer := multipart.NewWriter(&buffer) runUploadTest(t, content, "sample_exif.jpg", 200, func(w http.ResponseWriter, r *http.Request) {
file, err := writer.CreateFormFile("file", "test.jpg") err := r.ParseMultipartForm(100000)
require.NoError(t, err) require.NoError(t, err)
_, err = file.Write(content) size, err := strconv.Atoi(r.FormValue("file.size"))
require.NoError(t, err) 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")
err = writer.Close() w.WriteHeader(200)
fmt.Fprint(w, "RESPONSE")
})
}
func TestUploadHandlerRemovingExifTiff(t *testing.T) {
content, err := ioutil.ReadFile("exif/testdata/sample_exif.tiff")
require.NoError(t, err) require.NoError(t, err)
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) { runUploadTest(t, content, "sample_exif.tiff", 200, func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(100000) err := r.ParseMultipartForm(100000)
require.NoError(t, err) require.NoError(t, err)
...@@ -397,30 +399,36 @@ func TestUploadHandlerRemovingExif(t *testing.T) { ...@@ -397,30 +399,36 @@ func TestUploadHandlerRemovingExif(t *testing.T) {
w.WriteHeader(200) w.WriteHeader(200)
fmt.Fprint(w, "RESPONSE") fmt.Fprint(w, "RESPONSE")
}) })
defer ts.Close() }
httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", &buffer) func TestUploadHandlerRemovingExifInvalidContentType(t *testing.T) {
content, err := ioutil.ReadFile("exif/testdata/sample_exif_invalid.jpg")
require.NoError(t, err) require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background()) runUploadTest(t, content, "sample_exif_invalid.jpg", 200, func(w http.ResponseWriter, r *http.Request) {
defer cancel() err := r.ParseMultipartForm(100000)
require.NoError(t, err)
httpRequest = httpRequest.WithContext(ctx) output, err := ioutil.ReadFile(r.FormValue("file.path"))
httpRequest.ContentLength = int64(buffer.Len()) require.NoError(t, err)
httpRequest.Header.Set("Content-Type", writer.FormDataContentType()) require.Equal(t, content, output, "Expected the file to be same as before")
response := httptest.NewRecorder()
handler := newProxy(ts.URL) w.WriteHeader(200)
apiResponse := &api.Response{TempPath: tempPath} fmt.Fprint(w, "RESPONSE")
preparer := &DefaultPreparer{} })
opts, _, err := preparer.Prepare(apiResponse) }
func TestUploadHandlerRemovingExifCorruptedFile(t *testing.T) {
content, err := ioutil.ReadFile("exif/testdata/sample_exif_corrupted.jpg")
require.NoError(t, err) require.NoError(t, err)
HandleFileUploads(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts) runUploadTest(t, content, "sample_exif_corrupted.jpg", 422, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, 200, response.Code) err := r.ParseMultipartForm(100000)
require.Error(t, err)
})
} }
func TestUploadHandlerRemovingInvalidExif(t *testing.T) { func runUploadTest(t *testing.T, image []byte, filename string, httpCode int, tsHandler func(http.ResponseWriter, *http.Request)) {
tempPath, err := ioutil.TempDir("", "uploads") tempPath, err := ioutil.TempDir("", "uploads")
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll(tempPath) defer os.RemoveAll(tempPath)
...@@ -428,17 +436,16 @@ func TestUploadHandlerRemovingInvalidExif(t *testing.T) { ...@@ -428,17 +436,16 @@ func TestUploadHandlerRemovingInvalidExif(t *testing.T) {
var buffer bytes.Buffer var buffer bytes.Buffer
writer := multipart.NewWriter(&buffer) writer := multipart.NewWriter(&buffer)
file, err := writer.CreateFormFile("file", "test.jpg") file, err := writer.CreateFormFile("file", filename)
require.NoError(t, err)
_, err = file.Write(image)
require.NoError(t, err) require.NoError(t, err)
fmt.Fprint(file, "this is not valid image data")
err = writer.Close() err = writer.Close()
require.NoError(t, err) require.NoError(t, err)
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) { ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), tsHandler)
err := r.ParseMultipartForm(100000)
require.Error(t, err)
})
defer ts.Close() defer ts.Close()
httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", &buffer) httpRequest, err := http.NewRequest("POST", ts.URL+"/url/path", &buffer)
...@@ -459,7 +466,7 @@ func TestUploadHandlerRemovingInvalidExif(t *testing.T) { ...@@ -459,7 +466,7 @@ func TestUploadHandlerRemovingInvalidExif(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
HandleFileUploads(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts) HandleFileUploads(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts)
require.Equal(t, 422, response.Code) require.Equal(t, httpCode, response.Code)
} }
func newProxy(url string) *proxy.Proxy { func newProxy(url string) *proxy.Proxy {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment