##// END OF EJS Templates
Decompression bomb protection in image viewing
neko259 -
r1820:668d6c7d default
parent child Browse files
Show More
@@ -1,220 +1,230 b''
1 1 import re
2 2
3 from PIL import Image
4
3 5 from django.contrib.staticfiles import finders
4 6 from django.contrib.staticfiles.templatetags.staticfiles import static
5 7 from django.core.files.images import get_image_dimensions
6 8 from django.template.defaultfilters import filesizeformat
7 9 from django.core.urlresolvers import reverse
8 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
9 11
10 12 from boards.utils import get_domain, cached_result
11 13 from boards import settings
12 14
13 15
14 16 FILE_STUB_IMAGE = 'images/file.png'
15 17 FILE_STUB_URL = 'url'
16 18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
17 19
18 20
19 21 FILE_TYPES_VIDEO = (
20 22 'webm',
21 23 'mp4',
22 24 'mpeg',
23 25 'ogv',
24 26 )
25 27 FILE_TYPE_SVG = 'svg'
26 28 FILE_TYPES_AUDIO = (
27 29 'ogg',
28 30 'mp3',
29 31 'opus',
30 32 )
31 33 FILE_TYPES_IMAGE = (
32 34 'jpeg',
33 35 'jpg',
34 36 'png',
35 37 'bmp',
36 38 'gif',
37 39 )
38 40
39 41 PLAIN_FILE_FORMATS = {
40 42 'zip': 'archive',
41 43 'tar': 'archive',
42 44 'gz': 'archive',
43 45 'mid' : 'midi',
44 46 }
45 47
46 48 URL_PROTOCOLS = {
47 49 'magnet': 'magnet',
48 50 }
49 51
50 52 CSS_CLASS_IMAGE = 'image'
51 53 CSS_CLASS_THUMB = 'thumb'
52 54
53 55
54 56 def get_viewers():
55 57 return AbstractViewer.__subclasses__()
56 58
57 59
58 60 def get_static_dimensions(filename):
59 61 file_path = finders.find(filename)
60 62 return get_image_dimensions(file_path)
61 63
62 64
63 65 # TODO Move this to utils
64 66 def file_exists(filename):
65 67 return finders.find(filename) is not None
66 68
67 69
68 70 class AbstractViewer:
69 71 def __init__(self, file, file_type, hash, url):
70 72 self.file = file
71 73 self.file_type = file_type
72 74 self.hash = hash
73 75 self.url = url
74 76
75 77 @staticmethod
76 78 def supports(file_type):
77 79 return True
78 80
79 81 def get_view(self):
80 82 search_host = settings.get('External', 'ImageSearchHost')
81 83 if search_host:
82 84 search_url = search_host + self.file.url
83 85 else:
84 86 search_url = ''
85 87
86 88 return '<div class="image">'\
87 89 '{}'\
88 90 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
89 91 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-hash="{}">πŸ” </a></div>'\
90 92 '</div>'.format(self.get_format_view(), self.file.url,
91 93 self.file_type, filesizeformat(self.file.size),
92 94 self.file_type, search_url, self.hash)
93 95
94 96 def get_format_view(self):
95 97 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
96 98 file_name = FILE_FILEFORMAT.format(image_name)
97 99
98 100 if file_exists(file_name):
99 101 image = file_name
100 102 else:
101 103 image = FILE_STUB_IMAGE
102 104
103 105 w, h = get_static_dimensions(image)
104 106
105 107 return '<a href="{}">'\
106 108 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
107 109 '</a>'.format(self.file.url, static(image), w, h)
108 110
109 111
110 112 class VideoViewer(AbstractViewer):
111 113 @staticmethod
112 114 def supports(file_type):
113 115 return file_type in FILE_TYPES_VIDEO
114 116
115 117 def get_format_view(self):
116 118 return '<video width="200" height="150" controls src="{}"></video>'\
117 119 .format(self.file.url)
118 120
119 121
120 122 class AudioViewer(AbstractViewer):
121 123 @staticmethod
122 124 def supports(file_type):
123 125 return file_type in FILE_TYPES_AUDIO
124 126
125 127 def get_format_view(self):
126 128 return '<audio controls src="{}"></audio>'.format(self.file.url)
127 129
128 130
129 131 class SvgViewer(AbstractViewer):
130 132 @staticmethod
131 133 def supports(file_type):
132 134 return file_type == FILE_TYPE_SVG
133 135
134 136 def get_format_view(self):
135 137 return '<a class="thumb" href="{}">'\
136 138 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
137 139 '</a>'.format(self.file.url, self.file.url)
138 140
139 141
140 142 class ImageViewer(AbstractViewer):
141 143 @staticmethod
142 144 def supports(file_type):
143 145 return file_type in FILE_TYPES_IMAGE
144 146
145 147 def get_format_view(self):
146 148 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
147 149 filesizeformat(self.file.size))
148 width, height = get_image_dimensions(self.file.path)
150
151 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
152 try:
153 width, height = get_image_dimensions(self.file.path)
154 except Exception:
155 # If the image is a decompression bomb, treat it as just a regular
156 # file
157 return super().get_format_view()
158
149 159 preview_path = self.file.path.replace('.', '.200x150.')
150 160 pre_width, pre_height = get_image_dimensions(preview_path)
151 161
152 162 split = self.file.url.rsplit('.', 1)
153 163 w, h = 200, 150
154 164 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
155 165
156 166 return '<a class="{}" href="{full}">' \
157 167 '<img class="post-image-preview"' \
158 168 ' src="{}"' \
159 169 ' alt="{}"' \
160 170 ' width="{}"' \
161 171 ' height="{}"' \
162 172 ' data-width="{}"' \
163 173 ' data-height="{}" />' \
164 174 '</a>' \
165 175 .format(CSS_CLASS_THUMB,
166 176 thumb_url,
167 177 self.hash,
168 178 str(pre_width),
169 179 str(pre_height), str(width), str(height),
170 180 full=self.file.url, image_meta=metadata)
171 181
172 182
173 183 class UrlViewer(AbstractViewer):
174 184 @staticmethod
175 185 def supports(file_type):
176 186 return file_type is None
177 187
178 188 def get_view(self):
179 189 return '<div class="image">' \
180 190 '{}' \
181 191 '<div class="image-metadata">{}</div>' \
182 192 '</div>'.format(self.get_format_view(), get_domain(self.url))
183 193
184 194 def get_format_view(self):
185 195 protocol = self.url.split(':')[0]
186 196
187 197 domain = get_domain(self.url)
188 198
189 199 if protocol in URL_PROTOCOLS:
190 200 url_image_name = URL_PROTOCOLS.get(protocol)
191 201 elif domain:
192 202 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
193 203 else:
194 204 url_image_name = FILE_STUB_URL
195 205
196 206 image_path = 'images/{}.png'.format(url_image_name)
197 207 image = static(image_path)
198 208 w, h = get_static_dimensions(image_path)
199 209
200 210 return '<a href="{}">' \
201 211 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
202 212 '</a>'.format(self.url, image, w, h)
203 213
204 214 @cached_result()
205 215 def _find_image_for_domains(self, domain):
206 216 """
207 217 Searches for the domain image for every domain level except top.
208 218 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
209 219 example.co.uk, then co.uk
210 220 """
211 221 levels = domain.split('.')
212 222 while len(levels) > 1:
213 223 domain = '.'.join(levels)
214 224
215 225 filename = 'images/domains/{}.png'.format(domain)
216 226 if file_exists(filename):
217 227 return 'domains/' + domain
218 228 else:
219 229 del levels[0]
220 230
General Comments 0
You need to be logged in to leave comments. Login now