comparison roundup/cgi/templating.py @ 5201:a9ace22e0a2f

issue 2550690 - Adding anti-csrf measures to roundup following https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet and https://seclab.stanford.edu/websec/csrf/csrf.pdf Basically implement Synchronizer (CSRF) Tokens per form on a page. Single use (destroyed once used). Random input data for the token includes: system random implementation in python using /dev/urandom (fallback to random based on timestamp as the seed. Not as good, but should be ok for the short lifetime of the token??) the id (in cpython it's the memory address) of the object requesting a token. In theory this depends on memory layout, the history of the process (how many previous objects have been allocated from the heap etc.) I claim without any proof that for long running processes this is another source of randomness. For short running processes with little activity it could be guessed. last the floating point time.time() value is added. This may only have 1 second resolution so may be guessable. Hopefully for a short lived (2 week by default) token this is sufficient. Also in the current implementation the user is notified when validation fails and is told why. This allows the roundup admin to find the log entry (at error level) and try to resolve the issue. In the future user notification may change but for now this is probably best.
author John Rouillard <rouilj@ieee.org>
date Sat, 18 Mar 2017 16:59:01 -0400
parents cfd6d1f2caa1
children 47bd81998ddc
comparison
equal deleted inserted replaced
5200:16a8a3f0772c 5201:a9ace22e0a2f
21 21
22 22
23 import cgi, urllib, re, os.path, mimetypes, csv, string 23 import cgi, urllib, re, os.path, mimetypes, csv, string
24 import calendar 24 import calendar
25 import textwrap 25 import textwrap
26 import time, hashlib
26 27
27 from roundup import hyperdb, date, support 28 from roundup import hyperdb, date, support
28 from roundup import i18n 29 from roundup import i18n
29 from roundup.i18n import _ 30 from roundup.i18n import _
30 31
31 from KeywordsExpr import render_keywords_expression_editor 32 from KeywordsExpr import render_keywords_expression_editor
32 33
34 try:
35 # Use the cryptographic source of randomness if available
36 from random import SystemRandom
37 random=SystemRandom()
38 except ImportError:
39 from random import random
33 try: 40 try:
34 import cPickle as pickle 41 import cPickle as pickle
35 except ImportError: 42 except ImportError:
36 import pickle 43 import pickle
37 try: 44 try:
56 ### i18n services 63 ### i18n services
57 # this global translation service is not thread-safe. 64 # this global translation service is not thread-safe.
58 # it is left here for backward compatibility 65 # it is left here for backward compatibility
59 # until all Web UI translations are done via client.translator object 66 # until all Web UI translations are done via client.translator object
60 translationService = TranslationService.get_translation() 67 translationService = TranslationService.get_translation()
68
69 def anti_csrf_nonce(self, client, lifetime=None):
70 ''' Create a nonce for defending against CSRF attack.
71
72 This creates a nonce by hex encoding the sha256 of
73 random.random(), the address of the object requesting
74 the nonce and time.time().
75
76 Then it stores the nonce, the session id for the user
77 and the user id in the one time key database for use
78 by the csrf validator that runs in the client::inner_main
79 module/function.
80 '''
81 otks=client.db.getOTKManager()
82 # include id(self) as the exact location of self (including address)
83 # is unpredicatable (depends on number of previous connections etc.)
84 key = '%s%s%s'%(random.random(),id(self),time.time())
85 key = hashlib.sha256(key).hexdigest()
86
87 while otks.exists(key):
88 key = '%s%s%s'%(random.random(),id(self),time.time())
89 key = hashlib.sha256(key).hexdigest()
90
91 # lifetime is in minutes.
92 if lifetime is None:
93 lifetime = client.db.config['WEB_CSRF_TOKEN_LIFETIME']
94
95 # offset to time.time is calculated as:
96 # default lifetime is 1 week after __timestamp.
97 # That's the cleanup period hardcoded in otk.clean().
98 # If a user wants a 10 minute lifetime calculate
99 # 10 minutes newer than 1 week ago.
100 # lifetime - 10800 (number of minutes in a week)
101 # convert to seconds and add (possible negative number)
102 # from time.time().
103 otks.set(key, uid=client.db.getuid(),
104 sid=client.session_api._sid,
105 __timestamp=time.time() + ((lifetime - 10800) * 60) )
106 return key
61 107
62 ### templating 108 ### templating
63 109
64 class NoTemplate(Exception): 110 class NoTemplate(Exception):
65 pass 111 pass
723 Generate nothing if we're not editable. 769 Generate nothing if we're not editable.
724 """ 770 """
725 if not self.is_edit_ok(): 771 if not self.is_edit_ok():
726 return '' 772 return ''
727 773
728 return self.input(type="hidden", name="@action", value=action) + \ 774 return self.input(type="hidden", name="@csrf",
775 value=anti_csrf_nonce(self, self._client)) + \
776 '\n' + \
777 self.input(type="hidden", name="@action", value=action) + \
729 '\n' + \ 778 '\n' + \
730 self.input(type="submit", name="submit_button", value=self._(label)) 779 self.input(type="submit", name="submit_button", value=self._(label))
731 780
732 def history(self): 781 def history(self):
733 if not self.is_view_ok(): 782 if not self.is_view_ok():
861 """Generate a submit button. 910 """Generate a submit button.
862 911
863 Also sneak in the lastactivity and action hidden elements. 912 Also sneak in the lastactivity and action hidden elements.
864 """ 913 """
865 return self.input(type="hidden", name="@lastactivity", 914 return self.input(type="hidden", name="@lastactivity",
866 value=self.activity.local(0)) + '\n' + \ 915 value=self.activity.local(0)) + \
867 self.input(type="hidden", name="@action", value=action) + '\n' + \ 916 '\n' + \
868 self.input(type="submit", name="submit_button", value=self._(label)) 917 self.input(type="hidden", name="@csrf",
918 value=anti_csrf_nonce(self, self._client)) + \
919 '\n' + \
920 self.input(type="hidden", name="@action", value=action) + \
921 '\n' + \
922 self.input(type="submit", name="submit_button",
923 value=self._(label))
869 924
870 def journal(self, direction='descending'): 925 def journal(self, direction='descending'):
871 """ Return a list of HTMLJournalEntry instances. 926 """ Return a list of HTMLJournalEntry instances.
872 """ 927 """
873 # XXX do this 928 # XXX do this
2998 self.client = client 3053 self.client = client
2999 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): 3054 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
3000 return Batch(self.client, sequence, size, start, end, orphan, 3055 return Batch(self.client, sequence, size, start, end, orphan,
3001 overlap) 3056 overlap)
3002 3057
3058 def anti_csrf_nonce(self, lifetime=None):
3059 return anti_csrf_nonce(self, self.client, lifetime=lifetime)
3060
3003 def url_quote(self, url): 3061 def url_quote(self, url):
3004 """URL-quote the supplied text.""" 3062 """URL-quote the supplied text."""
3005 return urllib.quote(url) 3063 return urllib.quote(url)
3006 3064
3007 def html_quote(self, html): 3065 def html_quote(self, html):

Roundup Issue Tracker: http://roundup-tracker.org/