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