comparison roundup/cgi/client.py @ 7150:72a54826ff4f

better rest Origin check; refactor CORS preflight code. A previous version allowed requests without an origin that should require it (e.g. an OPTIONS or PATCH request). Moved the origin checking logic into the main flow. It looks like this was limited to OPTIONS/PATCH requests as handle_csrf() (called later in the main flow) handles POST, PUT, DELETE verbs. Refactored CORS preflight request code into functions and call them from main flow. Also return immediately. Prior code processed the options request a second time due to falling through. Modified is_origin_header_ok to return True if origin was missing and it was a get request. Fixed tests that make OPTIONS requests to supply origin. Comment fixups.
author John Rouillard <rouilj@ieee.org>
date Tue, 21 Feb 2023 16:42:20 -0500
parents 5c6dd791d638
children 1181157d7cec
comparison
equal deleted inserted replaced
7149:282ba72a5615 7150:72a54826ff4f
619 619
620 self.setHeader("Content-Type", "text/xml") 620 self.setHeader("Content-Type", "text/xml")
621 self.setHeader("Content-Length", str(len(output))) 621 self.setHeader("Content-Length", str(len(output)))
622 self.write(output) 622 self.write(output)
623 623
624 def is_cors_preflight(self):
625 return (
626 self.env['REQUEST_METHOD'] == "OPTIONS"
627 and self.request.headers.get("Access-Control-Request-Headers")
628 and self.request.headers.get("Access-Control-Request-Method")
629 and self.request.headers.get("Origin"))
630
631 def handle_preflight(self):
632 # Call rest library to handle the pre-flight request
633 handler = rest.RestfulInstance(self, self.db)
634 output = handler.dispatch(self.env['REQUEST_METHOD'],
635 self.path, self.form)
636
637 if self.response_code == 204:
638 self.write("")
639 else:
640 self.setHeader("Content-Length", str(len(output)))
641 self.write(output)
642
624 def handle_rest(self): 643 def handle_rest(self):
625 # Set the charset and language 644 # Set the charset and language
626 self.determine_charset() 645 self.determine_charset()
627 if self.instance.config["WEB_TRANSLATE_REST"]: 646 if self.instance.config["WEB_TRANSLATE_REST"]:
628 self.determine_language() 647 self.determine_language()
638 self.setHeader("Content-Length", str(len(output))) 657 self.setHeader("Content-Length", str(len(output)))
639 self.setHeader("Content-Type", "text/plain") 658 self.setHeader("Content-Type", "text/plain")
640 self.write(output) 659 self.write(output)
641 return 660 return
642 661
643 # allow preflight request even if unauthenticated 662 # verify Origin is allowed on all requests including GET.
644 if ( 663 # If a GET, missing origin is allowed (i.e. same site GET request)
645 self.env['REQUEST_METHOD'] == "OPTIONS" 664 if not self.is_origin_header_ok(api=True):
646 and self.request.headers.get("Access-Control-Request-Headers") 665 # Use code 400. Codes 401 and 403 imply that authentication
647 and self.request.headers.get("Access-Control-Request-Method") 666 # is needed or authenticated person is not authorized.
648 and self.request.headers.get("Origin") 667 # Preflight doesn't do authentication.
649 ): 668 self.response_code = 400
650 # verify Origin is allowed 669
651 if not self.is_origin_header_ok(api=True): 670 if 'HTTP_ORIGIN' not in self.env:
652 # Use code 400 as 401 and 403 imply that authentication 671 msg = self._("Required Header Missing")
653 # is needed or authenticates person is not authorized. 672 else:
654 # Preflight doesn't do authentication.
655 self.response_code = 400
656 msg = self._("Client is not allowed to use Rest Interface.") 673 msg = self._("Client is not allowed to use Rest Interface.")
657 output = s2b( 674
658 '{ "error": { "status": 400, "msg": "%s" } }' % msg) 675 output = s2b(
659 self.setHeader("Content-Length", str(len(output))) 676 '{ "error": { "status": 400, "msg": "%s" } }' % msg)
660 self.setHeader("Content-Type", "application/json") 677 self.setHeader("Content-Length", str(len(output)))
661 self.write(output) 678 self.setHeader("Content-Type", "application/json")
662 return 679 self.write(output)
663 680 return
664 # Call rest library to handle the pre-flight request 681
665 handler = rest.RestfulInstance(self, self.db) 682 # Handle CORS preflight request. We know rest is enabled
666 output = handler.dispatch(self.env['REQUEST_METHOD'], 683 # because handle_rest is called. Preflight requests
667 self.path, self.form) 684 # are unauthenticated, so no need to check permissions.
668 685 if ( self.is_cors_preflight() ):
686 self.handle_preflight()
687 return
669 elif not self.db.security.hasPermission('Rest Access', self.userid): 688 elif not self.db.security.hasPermission('Rest Access', self.userid):
670 self.response_code = 403 689 self.response_code = 403
671 output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }') 690 output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }')
672 self.setHeader("Content-Length", str(len(output))) 691 self.setHeader("Content-Length", str(len(output)))
673 self.setHeader("Content-Type", "application/json") 692 self.setHeader("Content-Type", "application/json")
678 697
679 try: 698 try:
680 # Call csrf with api (xmlrpc, rest) checks enabled. 699 # Call csrf with api (xmlrpc, rest) checks enabled.
681 # It will return True if everything is ok, 700 # It will return True if everything is ok,
682 # raises exception on check failure. 701 # raises exception on check failure.
702 # Note this returns true for a GET request.
703 # Must check supplied Origin header for bad value first.
683 csrf_ok = self.handle_csrf(api=True) 704 csrf_ok = self.handle_csrf(api=True)
684 except (Unauthorised, UsageError) as msg: 705 except (Unauthorised, UsageError) as msg:
685 # FIXME should return what the client requests 706 # FIXME should return what the client requests
686 # via accept header. 707 # via accept header.
687 output = s2b('{ "error": { "status": 400, "msg": "%s"}}' % 708 output = s2b('{ "error": { "status": 400, "msg": "%s"}}' %
693 csrf_ok = False # we had an error, failed check 714 csrf_ok = False # we had an error, failed check
694 return 715 return
695 716
696 # With the return above the if will never be false, 717 # With the return above the if will never be false,
697 # Keeping the if so we can remove return to pass 718 # Keeping the if so we can remove return to pass
698 # output though and format output according to accept 719 # output though and format output according to accept
699 # header. 720 # header.
700 if csrf_ok is True: 721 if csrf_ok is True:
701 # Call rest library to handle the request 722 # Call rest library to handle the request
702 handler = rest.RestfulInstance(self, self.db) 723 handler = rest.RestfulInstance(self, self.db)
703 output = handler.dispatch(self.env['REQUEST_METHOD'], 724 output = handler.dispatch(self.env['REQUEST_METHOD'],
1257 if not self.db.security.hasPermission('Web Access', self.userid): 1278 if not self.db.security.hasPermission('Web Access', self.userid):
1258 raise Unauthorised(self._("Anonymous users are not " 1279 raise Unauthorised(self._("Anonymous users are not "
1259 "allowed to use the web interface")) 1280 "allowed to use the web interface"))
1260 1281
1261 def is_origin_header_ok(self, api=False): 1282 def is_origin_header_ok(self, api=False):
1283 """Determine if origin is valid for the context
1284
1285 Allow (return True) if ORIGIN is missing and it is a GET.
1286 Allow if ORIGIN matches the base url.
1287 If this is a API call:
1288 Allow if ORIGIN matches an element of allowed_api_origins.
1289 Allow if allowed_api_origins includes '*' as first element..
1290 Otherwise disallow.
1291 """
1292
1262 try: 1293 try:
1263 origin = self.env['HTTP_ORIGIN'] 1294 origin = self.env['HTTP_ORIGIN']
1264 except KeyError: 1295 except KeyError:
1265 return False 1296 if self.env['REQUEST_METHOD'] == 'GET':
1297 return True
1298 else:
1299 return False
1266 1300
1267 # note base https://host/... ends host with with a /, 1301 # note base https://host/... ends host with with a /,
1268 # so add it to origin. 1302 # so add it to origin.
1269 foundat = self.base.find(origin + '/') 1303 foundat = self.base.find(origin + '/')
1270 if foundat == 0: 1304 if foundat == 0:

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