##// END OF EJS Templates
security: fix self-xss on modifing gist filename.
dan -
r1948:151fcf6c default
parent child Browse files
Show More
@@ -1,412 +1,412 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import peppercorn
26 26
27 27 from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPFound
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 35 from rhodecode.lib.utils2 import time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 38 from rhodecode.model.gist import GistModel
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.db import Gist, User, or_
41 41 from rhodecode.model import validation_schema
42 42 from rhodecode.model.validation_schema.schemas import gist_schema
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class GistView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 _ = self.request.translate
52 52 c = self._get_local_tmpl_context()
53 53 c.user = c.auth_user.get_instance()
54 54
55 55 c.lifetime_values = [
56 56 (-1, _('forever')),
57 57 (5, _('5 minutes')),
58 58 (60, _('1 hour')),
59 59 (60 * 24, _('1 day')),
60 60 (60 * 24 * 30, _('1 month')),
61 61 ]
62 62
63 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 64 c.acl_options = [
65 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 67 ]
68 68
69 69 self._register_global_c(c)
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @view_config(
74 74 route_name='gists_show', request_method='GET',
75 75 renderer='rhodecode:templates/admin/gists/index.mako')
76 76 def gist_show_all(self):
77 77 c = self.load_default_context()
78 78
79 79 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 80 c.show_private = self.request.GET.get('private') and not_default_user
81 81 c.show_public = self.request.GET.get('public') and not_default_user
82 82 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 83
84 84 gists = _gists = Gist().query()\
85 85 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 86 .order_by(Gist.created_on.desc())
87 87
88 88 c.active = 'public'
89 89 # MY private
90 90 if c.show_private and not c.show_public:
91 91 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 92 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 93 c.active = 'my_private'
94 94 # MY public
95 95 elif c.show_public and not c.show_private:
96 96 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 97 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 98 c.active = 'my_public'
99 99 # MY public+private
100 100 elif c.show_private and c.show_public:
101 101 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 102 Gist.gist_type == Gist.GIST_PRIVATE))\
103 103 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 104 c.active = 'my_all'
105 105 # Show all by super-admin
106 106 elif c.show_all:
107 107 c.active = 'all'
108 108 gists = _gists
109 109
110 110 # default show ALL public gists
111 111 if not c.show_public and not c.show_private and not c.show_all:
112 112 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 113 c.active = 'public'
114 114
115 115 _render = self.request.get_partial_renderer(
116 116 'data_table/_dt_elements.mako')
117 117
118 118 data = []
119 119
120 120 for gist in gists:
121 121 data.append({
122 122 'created_on': _render('gist_created', gist.created_on),
123 123 'created_on_raw': gist.created_on,
124 124 'type': _render('gist_type', gist.gist_type),
125 125 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 126 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 127 'author_raw': h.escape(gist.owner.full_contact),
128 128 'expires': _render('gist_expires', gist.gist_expires),
129 129 'description': _render('gist_description', gist.gist_description)
130 130 })
131 131 c.data = json.dumps(data)
132 132
133 133 return self._get_template_context(c)
134 134
135 135 @LoginRequired()
136 136 @NotAnonymous()
137 137 @view_config(
138 138 route_name='gists_new', request_method='GET',
139 139 renderer='rhodecode:templates/admin/gists/new.mako')
140 140 def gist_new(self):
141 141 c = self.load_default_context()
142 142 return self._get_template_context(c)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @CSRFRequired()
147 147 @view_config(
148 148 route_name='gists_create', request_method='POST',
149 149 renderer='rhodecode:templates/admin/gists/new.mako')
150 150 def gist_create(self):
151 151 _ = self.request.translate
152 152 c = self.load_default_context()
153 153
154 154 data = dict(self.request.POST)
155 155 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 156 data['nodes'] = [{
157 157 'filename': data['filename'],
158 158 'content': data.get('content'),
159 159 'mimetype': data.get('mimetype') # None is autodetect
160 160 }]
161 161
162 162 data['gist_type'] = (
163 163 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
164 164 data['gist_acl_level'] = (
165 165 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
166 166
167 167 schema = gist_schema.GistSchema().bind(
168 168 lifetime_options=[x[0] for x in c.lifetime_values])
169 169
170 170 try:
171 171
172 172 schema_data = schema.deserialize(data)
173 173 # convert to safer format with just KEYs so we sure no duplicates
174 174 schema_data['nodes'] = gist_schema.sequence_to_nodes(
175 175 schema_data['nodes'])
176 176
177 177 gist = GistModel().create(
178 178 gist_id=schema_data['gistid'], # custom access id not real ID
179 179 description=schema_data['description'],
180 180 owner=self._rhodecode_user.user_id,
181 181 gist_mapping=schema_data['nodes'],
182 182 gist_type=schema_data['gist_type'],
183 183 lifetime=schema_data['lifetime'],
184 184 gist_acl_level=schema_data['gist_acl_level']
185 185 )
186 186 Session().commit()
187 187 new_gist_id = gist.gist_access_id
188 188 except validation_schema.Invalid as errors:
189 189 defaults = data
190 190 errors = errors.asdict()
191 191
192 192 if 'nodes.0.content' in errors:
193 193 errors['content'] = errors['nodes.0.content']
194 194 del errors['nodes.0.content']
195 195 if 'nodes.0.filename' in errors:
196 196 errors['filename'] = errors['nodes.0.filename']
197 197 del errors['nodes.0.filename']
198 198
199 199 data = render('rhodecode:templates/admin/gists/new.mako',
200 200 self._get_template_context(c), self.request)
201 201 html = formencode.htmlfill.render(
202 202 data,
203 203 defaults=defaults,
204 204 errors=errors,
205 205 prefix_error=False,
206 206 encoding="UTF-8",
207 207 force_defaults=False
208 208 )
209 209 return Response(html)
210 210
211 211 except Exception:
212 212 log.exception("Exception while trying to create a gist")
213 213 h.flash(_('Error occurred during gist creation'), category='error')
214 214 raise HTTPFound(h.route_url('gists_new'))
215 215 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
216 216
217 217 @LoginRequired()
218 218 @NotAnonymous()
219 219 @CSRFRequired()
220 220 @view_config(
221 221 route_name='gist_delete', request_method='POST')
222 222 def gist_delete(self):
223 223 _ = self.request.translate
224 224 gist_id = self.request.matchdict['gist_id']
225 225
226 226 c = self.load_default_context()
227 227 c.gist = Gist.get_or_404(gist_id)
228 228
229 229 owner = c.gist.gist_owner == self._rhodecode_user.user_id
230 230 if not (h.HasPermissionAny('hg.admin')() or owner):
231 231 log.warning('Deletion of Gist was forbidden '
232 232 'by unauthorized user: `%s`', self._rhodecode_user)
233 233 raise HTTPNotFound()
234 234
235 235 GistModel().delete(c.gist)
236 236 Session().commit()
237 237 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
238 238
239 239 raise HTTPFound(h.route_url('gists_show'))
240 240
241 241 def _get_gist(self, gist_id):
242 242
243 243 gist = Gist.get_or_404(gist_id)
244 244
245 245 # Check if this gist is expired
246 246 if gist.gist_expires != -1:
247 247 if time.time() > gist.gist_expires:
248 248 log.error(
249 249 'Gist expired at %s', time_to_datetime(gist.gist_expires))
250 250 raise HTTPNotFound()
251 251
252 252 # check if this gist requires a login
253 253 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
254 254 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
255 255 log.error("Anonymous user %s tried to access protected gist `%s`",
256 256 self._rhodecode_user, gist_id)
257 257 raise HTTPNotFound()
258 258 return gist
259 259
260 260 @LoginRequired()
261 261 @view_config(
262 262 route_name='gist_show', request_method='GET',
263 263 renderer='rhodecode:templates/admin/gists/show.mako')
264 264 @view_config(
265 265 route_name='gist_show_rev', request_method='GET',
266 266 renderer='rhodecode:templates/admin/gists/show.mako')
267 267 @view_config(
268 268 route_name='gist_show_formatted', request_method='GET',
269 269 renderer=None)
270 270 @view_config(
271 271 route_name='gist_show_formatted_path', request_method='GET',
272 272 renderer=None)
273 273 def gist_show(self):
274 274 gist_id = self.request.matchdict['gist_id']
275 275
276 276 # TODO(marcink): expose those via matching dict
277 277 revision = self.request.matchdict.get('revision', 'tip')
278 278 f_path = self.request.matchdict.get('f_path', None)
279 279 return_format = self.request.matchdict.get('format')
280 280
281 281 c = self.load_default_context()
282 282 c.gist = self._get_gist(gist_id)
283 283 c.render = not self.request.GET.get('no-render', False)
284 284
285 285 try:
286 286 c.file_last_commit, c.files = GistModel().get_gist_files(
287 287 gist_id, revision=revision)
288 288 except VCSError:
289 289 log.exception("Exception in gist show")
290 290 raise HTTPNotFound()
291 291
292 292 if return_format == 'raw':
293 293 content = '\n\n'.join([f.content for f in c.files
294 294 if (f_path is None or f.path == f_path)])
295 295 response = Response(content)
296 296 response.content_type = 'text/plain'
297 297 return response
298 298
299 299 return self._get_template_context(c)
300 300
301 301 @LoginRequired()
302 302 @NotAnonymous()
303 303 @view_config(
304 304 route_name='gist_edit', request_method='GET',
305 305 renderer='rhodecode:templates/admin/gists/edit.mako')
306 306 def gist_edit(self):
307 307 _ = self.request.translate
308 308 gist_id = self.request.matchdict['gist_id']
309 309 c = self.load_default_context()
310 310 c.gist = self._get_gist(gist_id)
311 311
312 312 owner = c.gist.gist_owner == self._rhodecode_user.user_id
313 313 if not (h.HasPermissionAny('hg.admin')() or owner):
314 314 raise HTTPNotFound()
315 315
316 316 try:
317 317 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
318 318 except VCSError:
319 319 log.exception("Exception in gist edit")
320 320 raise HTTPNotFound()
321 321
322 322 if c.gist.gist_expires == -1:
323 323 expiry = _('never')
324 324 else:
325 325 # this cannot use timeago, since it's used in select2 as a value
326 326 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
327 327
328 328 c.lifetime_values.append(
329 329 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
330 330 )
331 331
332 332 return self._get_template_context(c)
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='gist_update', request_method='POST',
339 339 renderer='rhodecode:templates/admin/gists/edit.mako')
340 340 def gist_update(self):
341 341 _ = self.request.translate
342 342 gist_id = self.request.matchdict['gist_id']
343 343 c = self.load_default_context()
344 344 c.gist = self._get_gist(gist_id)
345 345
346 346 owner = c.gist.gist_owner == self._rhodecode_user.user_id
347 347 if not (h.HasPermissionAny('hg.admin')() or owner):
348 348 raise HTTPNotFound()
349 349
350 350 data = peppercorn.parse(self.request.POST.items())
351 351
352 352 schema = gist_schema.GistSchema()
353 353 schema = schema.bind(
354 354 # '0' is special value to leave lifetime untouched
355 355 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
356 356 )
357 357
358 358 try:
359 359 schema_data = schema.deserialize(data)
360 360 # convert to safer format with just KEYs so we sure no duplicates
361 361 schema_data['nodes'] = gist_schema.sequence_to_nodes(
362 362 schema_data['nodes'])
363 363
364 364 GistModel().update(
365 365 gist=c.gist,
366 366 description=schema_data['description'],
367 367 owner=c.gist.owner,
368 368 gist_mapping=schema_data['nodes'],
369 369 lifetime=schema_data['lifetime'],
370 370 gist_acl_level=schema_data['gist_acl_level']
371 371 )
372 372
373 373 Session().commit()
374 374 h.flash(_('Successfully updated gist content'), category='success')
375 375 except NodeNotChangedError:
376 376 # raised if nothing was changed in repo itself. We anyway then
377 377 # store only DB stuff for gist
378 378 Session().commit()
379 379 h.flash(_('Successfully updated gist data'), category='success')
380 380 except validation_schema.Invalid as errors:
381 errors = errors.asdict()
381 errors = h.escape(errors.asdict())
382 382 h.flash(_('Error occurred during update of gist {}: {}').format(
383 383 gist_id, errors), category='error')
384 384 except Exception:
385 385 log.exception("Exception in gist edit")
386 386 h.flash(_('Error occurred during update of gist %s') % gist_id,
387 387 category='error')
388 388
389 389 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
390 390
391 391 @LoginRequired()
392 392 @NotAnonymous()
393 393 @view_config(
394 394 route_name='gist_edit_check_revision', request_method='GET',
395 395 renderer='json_ext')
396 396 def gist_edit_check_revision(self):
397 397 _ = self.request.translate
398 398 gist_id = self.request.matchdict['gist_id']
399 399 c = self.load_default_context()
400 400 c.gist = self._get_gist(gist_id)
401 401
402 402 last_rev = c.gist.scm_instance().get_commit()
403 403 success = True
404 404 revision = self.request.GET.get('revision')
405 405
406 406 if revision != last_rev.raw_id:
407 407 log.error('Last revision %s is different then submitted %s'
408 408 % (revision, last_rev))
409 409 # our gist has newer version than we
410 410 success = False
411 411
412 412 return {'success': success}
General Comments 0
You need to be logged in to leave comments. Login now