Mercurial > p > roundup > code
comparison test/test_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 | 89b1870b1bc9 |
| children | f4b6a2a3e605 |
comparison
equal
deleted
inserted
replaced
| 5200:16a8a3f0772c | 5201:a9ace22e0a2f |
|---|---|
| 5 from test_actions import MockNull, true | 5 from test_actions import MockNull, true |
| 6 | 6 |
| 7 class MockDatabase(MockNull): | 7 class MockDatabase(MockNull): |
| 8 def getclass(self, name): | 8 def getclass(self, name): |
| 9 return self.classes[name] | 9 return self.classes[name] |
| 10 | |
| 11 # setup for csrf testing of otks database api | |
| 12 storage = {} | |
| 13 def set(self, key, **props): | |
| 14 MockDatabase.storage[key] = {} | |
| 15 MockDatabase.storage[key].update(props) | |
| 16 | |
| 17 def get(self, key, field, default=None): | |
| 18 if key not in MockDatabase.storage: | |
| 19 return default | |
| 20 return MockDatabase.storage[key][field] | |
| 21 | |
| 22 def exists(self,key): | |
| 23 return key in MockDatabase.storage | |
| 24 | |
| 25 def getOTKManager(self): | |
| 26 return MockDatabase() | |
| 10 | 27 |
| 11 class TemplatingTestCase(unittest.TestCase): | 28 class TemplatingTestCase(unittest.TestCase): |
| 12 def setUp(self): | 29 def setUp(self): |
| 13 self.form = FieldStorage() | 30 self.form = FieldStorage() |
| 14 self.client = MockNull() | 31 self.client = MockNull() |
| 15 self.client.db = db = MockDatabase() | 32 self.client.db = db = MockDatabase() |
| 16 db.security.hasPermission = lambda *args, **kw: True | 33 db.security.hasPermission = lambda *args, **kw: True |
| 17 self.client.form = self.form | 34 self.client.form = self.form |
| 35 | |
| 36 # add client props for testing anti_csrf_nonce | |
| 37 self.client.session_api = MockNull(_sid="1234567890") | |
| 38 self.client.db.getuid = lambda : 10 | |
| 39 self.client.db.config = {'WEB_CSRF_TOKEN_LIFETIME': 10 } | |
| 18 | 40 |
| 19 class HTMLDatabaseTestCase(TemplatingTestCase): | 41 class HTMLDatabaseTestCase(TemplatingTestCase): |
| 20 def test_HTMLDatabase___getitem__(self): | 42 def test_HTMLDatabase___getitem__(self): |
| 21 db = HTMLDatabase(self.client) | 43 db = HTMLDatabase(self.client) |
| 22 self.assert_(isinstance(db['issue'], HTMLClass)) | 44 self.assert_(isinstance(db['issue'], HTMLClass)) |
| 101 ( issue = MockNull(getprops = lambda : dict(nosy = nosy)) | 123 ( issue = MockNull(getprops = lambda : dict(nosy = nosy)) |
| 102 , user = MockNull(get = lambda id, name : id, lookup = lookup) | 124 , user = MockNull(get = lambda id, name : id, lookup = lookup) |
| 103 ) | 125 ) |
| 104 cls = HTMLClass(self.client, "issue") | 126 cls = HTMLClass(self.client, "issue") |
| 105 cls["nosy"] | 127 cls["nosy"] |
| 128 | |
| 129 def test_anti_csrf_nonce(self): | |
| 130 '''call the csrf creation function and do basic length test | |
| 131 | |
| 132 Store the data in a mock db with the same api as the otk | |
| 133 db. Make sure nonce is 64 chars long. Lookup the nonce in | |
| 134 db and retrieve data. Verify that the nonce lifetime is | |
| 135 correct (within 1 second of 1 week - lifetime), the uid is | |
| 136 correct (1), the dummy sid is correct. | |
| 137 | |
| 138 Consider three cases: | |
| 139 * create nonce via module function setting lifetime | |
| 140 * create nonce via TemplatingUtils method setting lifetime | |
| 141 * create nonce via module function with default lifetime | |
| 142 | |
| 143 ''' | |
| 144 | |
| 145 # the value below is number of seconds in a week. | |
| 146 week_seconds = 648000 | |
| 147 for test in [ 'module', 'template', 'default_time' ]: | |
| 148 if test == 'module': | |
| 149 # test the module function | |
| 150 nonce1 = anti_csrf_nonce(self, self.client, lifetime=1) | |
| 151 # lifetime * 60 is the offset | |
| 152 greater_than = week_seconds - 1 * 60 | |
| 153 elif test == 'template': | |
| 154 # call the function through the TemplatingUtils class | |
| 155 cls = TemplatingUtils(self.client) | |
| 156 nonce1 = cls.anti_csrf_nonce(lifetime=5) | |
| 157 greater_than = week_seconds - 5 * 60 | |
| 158 elif test == 'default_time': | |
| 159 # use the module function but with no lifetime | |
| 160 nonce1 = anti_csrf_nonce(self, self.client) | |
| 161 # see above for web nonce lifetime. | |
| 162 greater_than = week_seconds - 10 * 60 | |
| 163 | |
| 164 self.assertEqual(len(nonce1), 64) | |
| 165 otks=self.client.db.getOTKManager() | |
| 166 | |
| 167 uid = otks.get(nonce1, 'uid', default=None) | |
| 168 sid = otks.get(nonce1, 'sid', default=None) | |
| 169 timestamp = otks.get(nonce1, '__timestamp', default=None) | |
| 170 | |
| 171 self.assertEqual(uid, 10) | |
| 172 self.assertEqual(sid, self.client.session_api._sid) | |
| 173 | |
| 174 ts = time.time() | |
| 175 | |
| 176 # lower bound of the difference is above. Upper bound | |
| 177 # of difference is run time between time.time() in | |
| 178 # the call to anti_csrf_nonce and the time.time() call | |
| 179 # that assigns ts above. I declare that difference | |
| 180 # to be less than 1 second for this to pass. | |
| 181 self.assertEqual(True, | |
| 182 greater_than < ts - timestamp < (greater_than + 1) ) | |
| 183 | |
| 184 print "completed", test | |
| 106 | 185 |
| 107 def test_string_url_quote(self): | 186 def test_string_url_quote(self): |
| 108 ''' test that urlquote quotes the string ''' | 187 ''' test that urlquote quotes the string ''' |
| 109 p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'test string< foo@bar') | 188 p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'test string< foo@bar') |
| 110 self.assertEqual(p.url_quote(), 'test%20string%3C%20foo%40bar') | 189 self.assertEqual(p.url_quote(), 'test%20string%3C%20foo%40bar') |
| 360 def __getitem__(self, index): | 439 def __getitem__(self, index): |
| 361 def propchanged(self, property): | 440 def propchanged(self, property): |
| 362 def previous(self): | 441 def previous(self): |
| 363 def next(self): | 442 def next(self): |
| 364 | 443 |
| 365 class TemplatingUtils: | 444 #class TemplatingUtils: |
| 366 def __init__(self, client): | 445 # def __init__(self, client): |
| 367 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): | 446 # def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0): |
| 368 | 447 |
| 369 class NoTemplate(Exception): | 448 class NoTemplate(Exception): |
| 370 class Unauthorised(Exception): | 449 class Unauthorised(Exception): |
| 371 def __init__(self, action, klass): | 450 def __init__(self, action, klass): |
| 372 def __str__(self): | 451 def __str__(self): |
