├── 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 |
--------------------------------------------------------------------------------