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

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