diff 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
line wrap: on
line diff
--- a/roundup/cgi/templating.py	Sat Mar 18 15:12:39 2017 -0400
+++ b/roundup/cgi/templating.py	Sat Mar 18 16:59:01 2017 -0400
@@ -23,6 +23,7 @@
 import cgi, urllib, re, os.path, mimetypes, csv, string
 import calendar
 import textwrap
+import time, hashlib
 
 from roundup import hyperdb, date, support
 from roundup import i18n
@@ -30,6 +31,12 @@
 
 from KeywordsExpr import render_keywords_expression_editor
 
+try: 
+    # Use the cryptographic source of randomness if available
+    from random import SystemRandom
+    random=SystemRandom()
+except ImportError:
+    from random import random
 try:
     import cPickle as pickle
 except ImportError:
@@ -59,6 +66,45 @@
 # until all Web UI translations are done via client.translator object
 translationService = TranslationService.get_translation()
 
+def anti_csrf_nonce(self, client, lifetime=None):
+    ''' Create a nonce for defending against CSRF attack.
+
+        This creates a nonce by hex encoding the sha256 of
+        random.random(), the address of the object requesting
+        the nonce and time.time().
+
+        Then it stores the nonce, the session id for the user
+        and the user id in the one time key database for use
+        by the csrf validator that runs in the client::inner_main
+        module/function.
+    '''
+    otks=client.db.getOTKManager()
+    # include id(self) as the exact location of self (including address)
+    # is unpredicatable (depends on number of previous connections etc.)
+    key = '%s%s%s'%(random.random(),id(self),time.time())
+    key = hashlib.sha256(key).hexdigest()
+        
+    while otks.exists(key):
+        key = '%s%s%s'%(random.random(),id(self),time.time())
+        key = hashlib.sha256(key).hexdigest()
+
+    # lifetime is in minutes.
+    if lifetime is None:
+        lifetime = client.db.config['WEB_CSRF_TOKEN_LIFETIME']
+
+    # offset to time.time is calculated as:
+    #  default lifetime is 1 week after __timestamp.
+    # That's the cleanup period hardcoded in otk.clean().
+    # If a user wants a 10 minute lifetime calculate
+    # 10 minutes newer than 1 week ago.
+    #   lifetime - 10800 (number of minutes in a week)
+    # convert to seconds and add (possible negative number)
+    # from time.time().
+    otks.set(key, uid=client.db.getuid(),
+             sid=client.session_api._sid,
+             __timestamp=time.time() + ((lifetime - 10800) * 60) )
+    return key
+
 ### templating
 
 class NoTemplate(Exception):
@@ -725,7 +771,10 @@
         if not self.is_edit_ok():
             return ''
 
-        return self.input(type="hidden", name="@action", value=action) + \
+        return self.input(type="hidden", name="@csrf",
+                          value=anti_csrf_nonce(self, self._client)) + \
+            '\n' + \
+            self.input(type="hidden", name="@action", value=action) + \
             '\n' + \
             self.input(type="submit", name="submit_button", value=self._(label))
 
@@ -863,9 +912,15 @@
         Also sneak in the lastactivity and action hidden elements.
         """
         return self.input(type="hidden", name="@lastactivity",
-            value=self.activity.local(0)) + '\n' + \
-            self.input(type="hidden", name="@action", value=action) + '\n' + \
-            self.input(type="submit", name="submit_button", value=self._(label))
+            value=self.activity.local(0)) + \
+            '\n' + \
+            self.input(type="hidden", name="@csrf",
+                       value=anti_csrf_nonce(self, self._client)) + \
+            '\n' + \
+            self.input(type="hidden", name="@action", value=action) + \
+            '\n' + \
+            self.input(type="submit", name="submit_button",
+                       value=self._(label))
 
     def journal(self, direction='descending'):
         """ Return a list of HTMLJournalEntry instances.
@@ -3000,6 +3055,9 @@
         return Batch(self.client, sequence, size, start, end, orphan,
             overlap)
 
+    def anti_csrf_nonce(self, lifetime=None):
+        return anti_csrf_nonce(self, self.client, lifetime=lifetime)
+
     def url_quote(self, url):
         """URL-quote the supplied text."""
         return urllib.quote(url)

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