├── VERSION ├── testdata ├── test.gif ├── bad.png ├── test.jpg ├── test.png ├── bad_1N3.jpg └── bad_maybe.jpg ├── example └── main.go ├── .gitignore ├── LICENSE ├── README.md ├── secureimage_test.go └── secureimage.go /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 -------------------------------------------------------------------------------- /testdata/test.gif: -------------------------------------------------------------------------------- 1 | GIF89a; 2 | -------------------------------------------------------------------------------- /testdata/bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/secureimage/HEAD/testdata/bad.png -------------------------------------------------------------------------------- /testdata/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/secureimage/HEAD/testdata/test.jpg -------------------------------------------------------------------------------- /testdata/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/secureimage/HEAD/testdata/test.png -------------------------------------------------------------------------------- /testdata/bad_1N3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/secureimage/HEAD/testdata/bad_1N3.jpg -------------------------------------------------------------------------------- /testdata/bad_maybe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c1982/secureimage/HEAD/testdata/bad_maybe.jpg -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "secureimage" 7 | ) 8 | 9 | func main() { 10 | trusted, err := secureimage.Check(os.Args[1]) 11 | 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | if trusted { 17 | fmt.Println("yes.") 18 | } else { 19 | fmt.Println("bad image file") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | example/example 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oğuzhan YILMAZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secureimage 2 | TR: Go web uygulamalarında upload edilen resim dosyalarının güvenilir olup, olmadığını kontrol eden küçük bir doğrulama paketidir. 3 | Bu paket sadece gif, jpeg ve png dosya formatlarını doğrular. 4 | 5 | EN: This is a small verification package that checks whether image files uploaded in Go web applications are reliable. 6 | This package only supports gif, jpeg and png file formats. 7 | 8 | ## Install 9 | 10 | ```bash 11 | go get github.com/c1982/secureimage 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "github.com/c1982/secureimage" 23 | ) 24 | 25 | func main() { 26 | trusted, err := secureimage.Check("./uploads/tmp_test.jpg") 27 | 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | if trusted { 33 | fmt.Println("file is trusted.") 34 | } else { 35 | fmt.Println("bad file") 36 | } 37 | } 38 | ``` 39 | ## Todos 40 | 41 | - [x] Magic Bytes check 42 | - [x] Validate image file format 43 | - [x] Clean exif data in jpeg format 44 | 45 | ## Credits 46 | 47 | * [Oğuzhan](https://github.com/c1982) 48 | 49 | ## License 50 | 51 | The MIT License (MIT) - see LICENSE.md for more details -------------------------------------------------------------------------------- /secureimage_test.go: -------------------------------------------------------------------------------- 1 | package secureimage 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestCheckWhiteSpace(t *testing.T) { 10 | 11 | var list = []struct { 12 | b byte 13 | }{ 14 | {'\t'}, 15 | {'\n'}, 16 | {'\x0c'}, 17 | {'\r'}, 18 | {' '}, 19 | {'\v'}, 20 | {'\f'}, 21 | } 22 | 23 | for _, l := range list { 24 | if !isWhiteSpace(l.b) { 25 | t.Error("it is not expected:", l.b) 26 | break 27 | } 28 | } 29 | } 30 | 31 | func TestFirstByteNonSpace(t *testing.T) { 32 | 33 | str1 := ` 34 | 35 | GIF89a; 36 | zksdla` 37 | 38 | str2 := `firstbyte=0` 39 | 40 | str3 := " 0" 41 | 42 | data1 := []byte(str1) 43 | data2 := []byte(str2) 44 | data3 := []byte(str3) 45 | 46 | var list = []struct { 47 | data []byte 48 | seek int 49 | }{ 50 | {data1, 4}, 51 | {data2, 0}, 52 | {data3, 6}, 53 | } 54 | 55 | for _, l := range list { 56 | 57 | if i := firstByteNonWhiteSpace(l.data); i != l.seek { 58 | t.Error("invalid seek:", i) 59 | } 60 | } 61 | } 62 | 63 | func TestMatchMimePrefix(t *testing.T) { 64 | 65 | jpgData, _ := ioutil.ReadFile("./testdata/test.jpg") 66 | jpg2Data, _ := ioutil.ReadFile("./testdata/not_cleaned.jpg") 67 | pngData, _ := ioutil.ReadFile("./testdata/test.png") 68 | 69 | var list = []struct { 70 | ext string 71 | b []byte 72 | }{ 73 | {".gif", []byte("GIF89a;")}, 74 | {".gif", []byte("GIF89aõ^@ ^@÷ÿ^@2kÝ<91>0$°Ýîùl-þHÚ£e<9e>ùÙØú×îè0<9a>")}, 75 | {".jpg", jpgData}, 76 | {".jpg", jpg2Data}, 77 | {".png", pngData}, 78 | } 79 | 80 | for _, l := range list { 81 | if !matchMime(l.b, l.ext) { 82 | t.Error("invalid match prefix:", fmt.Sprintf("%c", l.b)) 83 | } 84 | } 85 | } 86 | 87 | func TestKnowExtension(t *testing.T) { 88 | 89 | if !knowedExtensions(".jpg") { 90 | t.Error("extension not found") 91 | } 92 | } 93 | 94 | func TestCheck(t *testing.T) { 95 | 96 | _, err := Check("./testdata/test.jpg") 97 | 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /secureimage.go: -------------------------------------------------------------------------------- 1 | package secureimage 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "image" 7 | "image/gif" 8 | "image/jpeg" 9 | "image/png" 10 | "io/ioutil" 11 | "os" 12 | "path" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | attentionPlease = 512 18 | ) 19 | 20 | var ( 21 | errUnknowExtension = errors.New("unknown file extension") 22 | errImageFileFormat = errors.New("invalid image format") 23 | signs map[string][][]byte 24 | extensions []string 25 | ) 26 | 27 | func init() { 28 | 29 | signs = map[string][][]byte{ 30 | ".gif": [][]byte{[]byte("GIF87a"), []byte("GIF89a")}, 31 | ".png": [][]byte{[]byte("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A")}, 32 | ".jpg": [][]byte{[]byte("\xFF\xD8\xFF")}, 33 | ".jpeg": [][]byte{[]byte("\xFF\xD8\xFF")}, 34 | } 35 | 36 | extensions = []string{".jpg", ".jpeg", ".gif", ".png"} 37 | } 38 | 39 | //Check you can check trusted image file. 40 | func Check(imagefile string) (trusted bool, err error) { 41 | 42 | trusted = false 43 | f, err := os.Open(imagefile) 44 | 45 | if err != nil { 46 | return 47 | } 48 | 49 | defer f.Close() 50 | data, err := ioutil.ReadAll(f) 51 | 52 | ext := path.Ext(imagefile) 53 | ext = strings.ToLower(ext) 54 | 55 | if !knowedExtensions(ext) { 56 | return trusted, errUnknowExtension 57 | } 58 | 59 | if !matchMime(data, ext) { 60 | return trusted, errImageFileFormat 61 | } 62 | 63 | img, err := checkIt(f, ext) 64 | 65 | if err != nil { 66 | return trusted, err 67 | } 68 | 69 | return cleanIt(imagefile, ext, img) 70 | } 71 | 72 | func checkIt(f *os.File, ext string) (img image.Image, err error) { 73 | 74 | f.Seek(0, 0) 75 | 76 | switch ext { 77 | case ".png": 78 | img, err = png.Decode(f) 79 | case ".jpg", ".jpeg": 80 | img, err = jpeg.Decode(f) 81 | case ".gif": 82 | img, err = gif.Decode(f) 83 | default: 84 | err = errUnknowExtension 85 | } 86 | 87 | if err != nil { 88 | return img, err 89 | } 90 | 91 | return img, nil 92 | } 93 | 94 | func cleanIt(filename, ext string, img image.Image) (isok bool, err error) { 95 | 96 | cleaned, err := os.Create(filename) 97 | 98 | if err != nil { 99 | return false, err 100 | } 101 | 102 | defer cleaned.Close() 103 | 104 | switch ext { 105 | case ".png": 106 | err = png.Encode(cleaned, img) 107 | case ".jpg", ".jpeg": 108 | err = jpeg.Encode(cleaned, img, nil) 109 | case ".gif": 110 | err = gif.Encode(cleaned, img, nil) 111 | default: 112 | err = errUnknowExtension 113 | } 114 | 115 | if err != nil { 116 | return false, err 117 | } 118 | 119 | return true, nil 120 | } 121 | 122 | func matchMime(data []byte, ext string) bool { 123 | 124 | if len(data) > attentionPlease { 125 | data = data[:attentionPlease] 126 | } 127 | 128 | prefixes := signs[ext] 129 | 130 | i := firstByteNonWhiteSpace(data) 131 | data = data[i:] 132 | 133 | for i := 0; i < len(prefixes); i++ { 134 | if bytes.HasPrefix(data, prefixes[i]) { 135 | return true 136 | } 137 | } 138 | 139 | return false 140 | } 141 | 142 | func firstByteNonWhiteSpace(data []byte) (nonws int) { 143 | 144 | nonws = 0 145 | for ; nonws < len(data) && isWhiteSpace(data[nonws]); nonws++ { 146 | } 147 | 148 | return 149 | } 150 | 151 | func isWhiteSpace(b byte) bool { 152 | 153 | switch b { 154 | case '\t', '\n', '\r', '\v', '\f', ' ': 155 | return true 156 | } 157 | 158 | return false 159 | } 160 | 161 | func knowedExtensions(ext string) bool { 162 | 163 | for i := 0; i < len(extensions); i++ { 164 | if ext == extensions[i] { 165 | return true 166 | } 167 | } 168 | 169 | return false 170 | } 171 | --------------------------------------------------------------------------------