comparison roundup/rest.py @ 5726:e199d0ae4a25

issue2551033: prevent reverse engineering hidden data by using etags as an oracle to identify when the right data has been guessed. Identified by Joseph Myers who also suggested remediation methods. Implemented John Rouillard.
author John Rouillard <rouilj@ieee.org>
date Thu, 23 May 2019 18:56:57 -0400
parents d9a3f6957731
children 8b5171f353eb
comparison
equal deleted inserted replaced
5725:6923225fd781 5726:e199d0ae4a25
35 from roundup import actions 35 from roundup import actions
36 from roundup.anypy.strings import bs2b, b2s, u2s, is_us 36 from roundup.anypy.strings import bs2b, b2s, u2s, is_us
37 from roundup.exceptions import * 37 from roundup.exceptions import *
38 from roundup.cgi.exceptions import * 38 from roundup.cgi.exceptions import *
39 39
40 from hashlib import md5 40 import hmac
41 41
42 # Py3 compatible basestring 42 # Py3 compatible basestring
43 try: 43 try:
44 basestring 44 basestring
45 except NameError: 45 except NameError:
116 'data': data 116 'data': data
117 } 117 }
118 return result 118 return result
119 return format_object 119 return format_object
120 120
121 def calculate_etag (node, classname="Missing", id="0"): 121 def calculate_etag (node, key, classname="Missing", id="0"):
122 '''given a hyperdb node generate a hashed representation of it to be 122 '''given a hyperdb node generate a hashed representation of it to be
123 used as an etag. 123 used as an etag.
124 124
125 This code needs a __repr__ function in the Password class. This 125 This code needs a __repr__ function in the Password class. This
126 replaces the repr(items) which would be: 126 replaces the repr(items) which would be:
140 140
141 classname and id are used for logging only. 141 classname and id are used for logging only.
142 ''' 142 '''
143 143
144 items = node.items(protected=True) # include every item 144 items = node.items(protected=True) # include every item
145 etag = md5(bs2b(repr(sorted(items)))).hexdigest() 145 etag = hmac.new(bs2b(repr(sorted(items)))).hexdigest()
146 logger.debug("object=%s%s; tag=%s; repr=%s", classname, id, 146 logger.debug("object=%s%s; tag=%s; repr=%s", classname, id,
147 etag, repr(node.items(protected=True))) 147 etag, repr(node.items(protected=True)))
148 # Quotes are part of ETag spec, normal headers don't have quotes 148 # Quotes are part of ETag spec, normal headers don't have quotes
149 return '"%s"' % etag 149 return '"%s"' % etag
150 150
151 def check_etag (node, etags, classname="Missing", id="0"): 151 def check_etag (node, key, etags, classname="Missing", id="0"):
152 '''Take a list of etags and compare to the etag for the given node. 152 '''Take a list of etags and compare to the etag for the given node.
153 153
154 Iterate over all supplied etags, 154 Iterate over all supplied etags,
155 If a tag fails to match, return False. 155 If a tag fails to match, return False.
156 If at least one etag matches, return True. 156 If at least one etag matches, return True.
157 If all etags are None, return False. 157 If all etags are None, return False.
158 158
159 ''' 159 '''
160 have_etag_match=False 160 have_etag_match=False
161 161
162 node_etag = calculate_etag(node, classname, id) 162 node_etag = calculate_etag(node, key, classname, id)
163 163
164 for etag in etags: 164 for etag in etags:
165 if etag is not None: 165 if etag is not None:
166 if etag != node_etag: 166 if etag != node_etag:
167 return False 167 return False
509 return result 509 return result
510 510
511 def raise_if_no_etag(self, class_name, item_id, input): 511 def raise_if_no_etag(self, class_name, item_id, input):
512 class_obj = self.db.getclass(class_name) 512 class_obj = self.db.getclass(class_name)
513 if not check_etag(class_obj.getnode(item_id), 513 if not check_etag(class_obj.getnode(item_id),
514 self.db.config.WEB_SECRET_KEY,
514 obtain_etags(self.client.request.headers, input), 515 obtain_etags(self.client.request.headers, input),
515 class_name, 516 class_name,
516 item_id): 517 item_id):
517 raise PreconditionFailed( 518 raise PreconditionFailed(
518 "If-Match is missing or does not match." 519 "If-Match is missing or does not match."
785 raise Unauthorised( 786 raise Unauthorised(
786 'Permission to view %s%s denied' % (class_name, itemid) 787 'Permission to view %s%s denied' % (class_name, itemid)
787 ) 788 )
788 789
789 node = class_obj.getnode(itemid) 790 node = class_obj.getnode(itemid)
790 etag = calculate_etag(node, class_name, itemid) 791 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY,
792 class_name, itemid)
791 props = None 793 props = None
792 protected=False 794 protected=False
793 verbose=1 795 verbose=1
794 for form_field in input.value: 796 for form_field in input.value:
795 key = form_field.name 797 key = form_field.name
866 (class_name, item_id, attr_name) 868 (class_name, item_id, attr_name)
867 ) 869 )
868 870
869 class_obj = self.db.getclass(class_name) 871 class_obj = self.db.getclass(class_name)
870 node = class_obj.getnode(item_id) 872 node = class_obj.getnode(item_id)
871 etag = calculate_etag(node, class_name, item_id) 873 etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY,
874 class_name, item_id)
872 data = node.__getattr__(attr_name) 875 data = node.__getattr__(attr_name)
873 result = { 876 result = {
874 'id': item_id, 877 'id': item_id,
875 'type': str(type(data)), 878 'type': str(type(data)),
876 'link': "%s/%s/%s/%s" % 879 'link': "%s/%s/%s/%s" %

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