Skip to content

Commit 36d0f61

Browse files
committed
fix(api): issue2551384. Verify REST authorization earlier
To reduce the ability of bad actors to spam (DOS) the REST endpoint with bad data and generate logs meant for debugging, modify the flow in client.py's REST handler to verify authorization earlier. If the anonymous user is allowed to use REST, this won't make a difference for a DOS attempt. The templates don't enable REST for the anonymous user by default. Most admins don't change this. The validation order for REST requests has been changed. CORS identfied an handled User authorization to use REST (return 403 on failure) REST request validated (Origin header valid etc.) (return 400 for bad request) Incorrectly formatted CORS preflight requests (e.g. missing Origin header) that are not recogized as a CORS request can now return HTTP status 403 as well as status 400 (when anonymous is allowed access). Note all CORS preflights are sent without authentication so appear as anonymous requests. The tests were updated to compensate, but it is not obvious to me from specs what the proper evaulation order/return codes should be for this case. Both 403/400 are failures and cause CORS to fail so there should be no difference but...
1 parent f74deec commit 36d0f61

File tree

5 files changed

+49
-19
lines changed

5 files changed

+49
-19
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ Fixed:
6060
on command line. (John Rouillard)
6161
- issue2551374 - Add error handling for filter expressions. Filter
6262
expression errors are now reported. (John Rouillard)
63+
- issue2551384: Modify flow in client.py's REST handler to verify
64+
authorization earlier. The validation order for REST requests
65+
has been changed. Checking user authorization to use the REST
66+
interface is done before validating the Origin header. As a
67+
result, incorrectly formatted CORS preflight requests
68+
(e.g. missing Origin header) can now return HTTP status 403 as
69+
well as status 400. (John Rouillard)
6370

6471
Features:
6572

doc/upgrading.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ defusedxml.
224224

225225
.. _defusedxml: https://pypi.org/project/defusedxml/
226226

227+
Change in REST response for invalid CORS requests (info)
228+
--------------------------------------------------------
229+
230+
CORS_ preflight requests that are missing required headers can
231+
now result in either a 403 or 400 error code. If you permit
232+
anonymous users to access the REST interface, a 400 error may
233+
still occur. Previously, only a 400 error was given. This change
234+
is not expected to create issues since the client will recognize
235+
both codes it as an error response, and the CORS request will
236+
still fail.
237+
227238
More secure session cookie handling (info)
228239
------------------------------------------
229240

roundup/cgi/client.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -677,8 +677,11 @@ def handle_xmlrpc(self):
677677
def is_cors_preflight(self):
678678
return (
679679
self.env['REQUEST_METHOD'] == "OPTIONS"
680-
and self.request.headers.get("Access-Control-Request-Headers")
681680
and self.request.headers.get("Access-Control-Request-Method")
681+
# technically Access-Control-Request-Headers (ACRH) is
682+
# optional, but we require the header x-requested-with,
683+
# so ACRH will be present.
684+
and self.request.headers.get("Access-Control-Request-Headers")
682685
and self.request.headers.get("Origin"))
683686

684687
def handle_preflight(self):
@@ -721,6 +724,30 @@ def handle_rest(self):
721724
status=http_.client.TOO_MANY_REQUESTS)
722725
return
723726

727+
# Handle CORS preflight request. We know rest is enabled
728+
# because handle_rest is called. Preflight requests
729+
# are unauthenticated, so no need to check permissions.
730+
if (self.is_cors_preflight()):
731+
# Origin header must be defined to get here
732+
if self.is_origin_header_ok(api=True):
733+
self.handle_preflight()
734+
else:
735+
# origin is not authorized for REST
736+
msg = self._("Client is not allowed to use Rest Interface.")
737+
output = s2b(
738+
'{ "error": { "status": 400, "msg": "%s" } }' % msg)
739+
self.reject_request(output,
740+
message_type="application/json",
741+
status=400)
742+
return
743+
744+
if not self.db.security.hasPermission('Rest Access', self.userid):
745+
output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }')
746+
self.reject_request(output,
747+
message_type="application/json",
748+
status=403)
749+
return
750+
724751
# verify Origin is allowed on all requests including GET.
725752
# If a GET, missing origin is allowed (i.e. same site GET request)
726753
if not self.is_origin_header_ok(api=True):
@@ -753,20 +780,6 @@ def handle_rest(self):
753780
"origin": self.env.get('HTTP_ORIGIN', None)})
754781
return
755782

756-
# Handle CORS preflight request. We know rest is enabled
757-
# because handle_rest is called. Preflight requests
758-
# are unauthenticated, so no need to check permissions.
759-
if (self.is_cors_preflight()):
760-
self.handle_preflight()
761-
return
762-
763-
if not self.db.security.hasPermission('Rest Access', self.userid):
764-
output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }')
765-
self.reject_request(output,
766-
message_type="application/json",
767-
status=403)
768-
return
769-
770783
self.check_anonymous_access()
771784

772785
try:

test/test_cgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@ def wh(s):
13601360
cl.additional_headers
13611361
)
13621362

1363-
self.assertEqual(cl.response_code, 400)
1363+
self.assertEqual(cl.response_code, 403)
13641364
del(out[0])
13651365

13661366
# origin not set to allowed value

test/test_liveserver.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,10 +711,9 @@ def test_rest_preflight_collection(self):
711711
"x-requested-with",
712712
'Access-Control-Request-Method': "PUT",})
713713

714-
self.assertEqual(f.status_code, 400)
714+
self.assertEqual(f.status_code, 403)
715715

716-
expected = ('{ "error": { "status": 400, "msg": "Required'
717-
' Header Missing" } }')
716+
expected = ('{ "error": { "status": 403, "msg": "Forbidden." } }')
718717
self.assertEqual(b2s(f.content), expected)
719718

720719

0 commit comments

Comments
 (0)