Mercurial > p > roundup > code
comparison roundup/cgi/client.py @ 6681:ab2ed11c021e
issue2551205: Add support for specifying valid origins for api: xmlrpc/rest
We now have an allow list to filter the hosts allowed to do api
requests. An element of this allow list must match the http ORIGIN
header exactly or the rest/xmlrpc CORS request will result in an
error.
The tracker host is always allowed to do a request.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Tue, 17 May 2022 17:18:51 -0400 |
| parents | 408fd477761f |
| children | 9a1f5e496e6c |
comparison
equal
deleted
inserted
replaced
| 6680:b4d0b48b3096 | 6681:ab2ed11c021e |
|---|---|
| 579 # coverting from function returning true/false to | 579 # coverting from function returning true/false to |
| 580 # raising exceptions | 580 # raising exceptions |
| 581 # Call csrf with xmlrpc checks enabled. | 581 # Call csrf with xmlrpc checks enabled. |
| 582 # It will return True if everything is ok, | 582 # It will return True if everything is ok, |
| 583 # raises exception on check failure. | 583 # raises exception on check failure. |
| 584 csrf_ok = self.handle_csrf(xmlrpc=True) | 584 csrf_ok = self.handle_csrf(api=True) |
| 585 except (Unauthorised, UsageError) as msg: | 585 except (Unauthorised, UsageError) as msg: |
| 586 # report exception back to server | 586 # report exception back to server |
| 587 exc_type, exc_value, exc_tb = sys.exc_info() | 587 exc_type, exc_value, exc_tb = sys.exc_info() |
| 588 output = xmlrpc_.client.dumps( | 588 output = xmlrpc_.client.dumps( |
| 589 xmlrpc_.client.Fault(1, "%s:%s" % (exc_type, exc_value)), | 589 xmlrpc_.client.Fault(1, "%s:%s" % (exc_type, exc_value)), |
| 629 return | 629 return |
| 630 | 630 |
| 631 self.check_anonymous_access() | 631 self.check_anonymous_access() |
| 632 | 632 |
| 633 try: | 633 try: |
| 634 # Call csrf with xmlrpc checks enabled. | 634 # Call csrf with api (xmlrpc, rest) checks enabled. |
| 635 # It will return True if everything is ok, | 635 # It will return True if everything is ok, |
| 636 # raises exception on check failure. | 636 # raises exception on check failure. |
| 637 csrf_ok = self.handle_csrf(xmlrpc=True) | 637 csrf_ok = self.handle_csrf(api=True) |
| 638 except (Unauthorised, UsageError) as msg: | 638 except (Unauthorised, UsageError) as msg: |
| 639 # report exception back to server | 639 # report exception back to server |
| 640 exc_type, exc_value, exc_tb = sys.exc_info() | 640 exc_type, exc_value, exc_tb = sys.exc_info() |
| 641 # FIXME should return what the client requests | 641 # FIXME should return what the client requests |
| 642 # via accept header. | 642 # via accept header. |
| 1205 if self.user == 'anonymous': | 1205 if self.user == 'anonymous': |
| 1206 if not self.db.security.hasPermission('Web Access', self.userid): | 1206 if not self.db.security.hasPermission('Web Access', self.userid): |
| 1207 raise Unauthorised(self._("Anonymous users are not " | 1207 raise Unauthorised(self._("Anonymous users are not " |
| 1208 "allowed to use the web interface")) | 1208 "allowed to use the web interface")) |
| 1209 | 1209 |
| 1210 | 1210 def is_origin_header_ok(self, api=False): |
| 1211 def handle_csrf(self, xmlrpc=False): | 1211 origin = self.env['HTTP_ORIGIN'] |
| 1212 # note base https://host/... ends host with with a /, | |
| 1213 # so add it to origin. | |
| 1214 foundat = self.base.find(origin +'/') | |
| 1215 if foundat == 0: | |
| 1216 return True | |
| 1217 | |
| 1218 if not api: | |
| 1219 return False | |
| 1220 | |
| 1221 allowed_origins = self.db.config['WEB_ALLOWED_API_ORIGINS'] | |
| 1222 # find a match for other possible origins | |
| 1223 # Original spec says origin is case sensitive match. | |
| 1224 # Living spec doesn't address Origin value's case or | |
| 1225 # how to compare it. So implement case sensitive.... | |
| 1226 if allowed_origins[0] == '*' or origin in allowed_origins: | |
| 1227 return True | |
| 1228 | |
| 1229 return False | |
| 1230 | |
| 1231 def handle_csrf(self, api=False): | |
| 1212 '''Handle csrf token lookup and validate current user and session | 1232 '''Handle csrf token lookup and validate current user and session |
| 1213 | 1233 |
| 1214 This implements (or tries to implement) the | 1234 This implements (or tries to implement) the |
| 1215 Session-Dependent Nonce from | 1235 Session-Dependent Nonce from |
| 1216 https://seclab.stanford.edu/websec/csrf/csrf.pdf. | 1236 https://seclab.stanford.edu/websec/csrf/csrf.pdf. |
| 1330 # if you change these make sure to consider what | 1350 # if you change these make sure to consider what |
| 1331 # happens if header variable exists but is empty. | 1351 # happens if header variable exists but is empty. |
| 1332 # self.base.find("") returns 0 for example not -1 | 1352 # self.base.find("") returns 0 for example not -1 |
| 1333 enforce=config['WEB_CSRF_ENFORCE_HEADER_ORIGIN'] | 1353 enforce=config['WEB_CSRF_ENFORCE_HEADER_ORIGIN'] |
| 1334 if 'HTTP_ORIGIN' in self.env and enforce != "no": | 1354 if 'HTTP_ORIGIN' in self.env and enforce != "no": |
| 1335 origin = self.env['HTTP_ORIGIN'] | 1355 if not self.is_origin_header_ok(api=api): |
| 1336 foundat = self.base.find(origin +'/') | 1356 origin = self.env['HTTP_ORIGIN'] |
| 1337 if foundat != 0: | |
| 1338 if enforce in ('required', 'yes'): | 1357 if enforce in ('required', 'yes'): |
| 1339 logger.error(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin) | 1358 logger.error(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin) |
| 1340 raise Unauthorised(self._("Invalid Origin %s"%origin)) | 1359 raise Unauthorised(self._("Invalid Origin %s"%origin)) |
| 1341 elif enforce == 'logfailure': | 1360 elif enforce == 'logfailure': |
| 1342 logger.warning(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin) | 1361 logger.warning(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin) |
| 1382 if header_pass < enforce: | 1401 if header_pass < enforce: |
| 1383 logger.error(self._("Csrf: unable to verify sufficient headers")) | 1402 logger.error(self._("Csrf: unable to verify sufficient headers")) |
| 1384 raise UsageError(self._("Unable to verify sufficient headers")) | 1403 raise UsageError(self._("Unable to verify sufficient headers")) |
| 1385 | 1404 |
| 1386 enforce=config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] | 1405 enforce=config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] |
| 1387 if xmlrpc: | 1406 if api: |
| 1388 if enforce in ['required', 'yes']: | 1407 if enforce in ['required', 'yes']: |
| 1389 # if we get here we have usually passed at least one | 1408 # if we get here we have usually passed at least one |
| 1390 # header check. We check for presence of this custom | 1409 # header check. We check for presence of this custom |
| 1391 # header for xmlrpc calls only. | 1410 # header for xmlrpc/rest calls only. |
| 1392 # E.G. X-Requested-With: XMLHttpRequest | 1411 # E.G. X-Requested-With: XMLHttpRequest |
| 1393 # Note we do not use CSRF nonces for xmlrpc requests. | 1412 # Note we do not use CSRF nonces for xmlrpc/rest requests. |
| 1394 # | 1413 # |
| 1395 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers | 1414 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers |
| 1396 if 'HTTP_X_REQUESTED_WITH' not in self.env: | 1415 if 'HTTP_X_REQUESTED_WITH' not in self.env: |
| 1397 logger.error(self._("csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s."), current_user) | 1416 logger.error(self._("csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s."), current_user) |
| 1398 raise UsageError(self._("Required Header Missing")) | 1417 raise UsageError(self._("Required Header Missing")) |
| 1403 # once an hour. If we have short lived (e.g. 5 minute) keys | 1422 # once an hour. If we have short lived (e.g. 5 minute) keys |
| 1404 # they will live too long if we depend on clean_up. So we do | 1423 # they will live too long if we depend on clean_up. So we do |
| 1405 # our own. | 1424 # our own. |
| 1406 otks.clean() | 1425 otks.clean() |
| 1407 | 1426 |
| 1408 if xmlrpc: | 1427 if api: |
| 1409 # Save removal of expired keys from database. | 1428 # Save removal of expired keys from database. |
| 1410 otks.commit() | 1429 otks.commit() |
| 1411 # Return from here since we have done housekeeping | 1430 # Return from here since we have done housekeeping |
| 1412 # and don't use csrf tokens for xmlrpc. | 1431 # and don't use csrf tokens for xmlrpc. |
| 1413 return True | 1432 return True |
