Mercurial > p > roundup > code
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" % |
