Mercurial > p > roundup > code
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): |
