Skip to content

Commit 5220f36

Browse files
committed
improve REST interface security
When using REST, we reflect the client's origin. If the wildcard '*' is used in allowed_api_origins all origins are allowed. When this is done, it also added an 'Access-Control-Allow-Credentials: true' header. This Credentials header should not be added if the site is matched only by '*'. This header should be provided only for explicit origins (e.g. https://example.org) not for the wildcard. This is now fixed for CORS preflight OPTIONS request as well as normal GET, PUT, DELETE, POST, PATCH and OPTIONS requests. A missing Access-Control-Allow-Credentials will prevent the tracker from being accessed using credentials. This prevents an unauthorized third party web site from using a user's credentials to access information in the tracker that is not publicly available. Added test for this specific case. In addition, allowed_api_origins can include explicit origins in addition to '*'. '*' must be first in the list. Also adapted numerous tests to work with these changes. Doc updates.
1 parent 7353cb3 commit 5220f36

File tree

9 files changed

+282
-42
lines changed

9 files changed

+282
-42
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ Fixed:
6565
Schlatterbeck)
6666
- Update some template schema files to assign Register permissions for the
6767
Anonymous user. Replaces the old Create permission. (John Rouillard)
68+
- Allow '*' and explicit origins in allowed_api_origins. Only return
69+
'Access-Control-Allow-Credentials' when not matching '*'. Fixes
70+
security issue with rest when using '*'.
6871

6972
Features:
7073

doc/rest.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,26 @@ the browser and must all be present:
231231
* `Access-Control-Request-Method`
232232
* `Origin`
233233

234-
The 204 response will include the headers:
234+
The headers of the 204 response depend on the
235+
``allowed_api_origins`` setting. If a ``*`` is included as the
236+
first element, any client can read the data but they can not
237+
provide authentication. This limits the available data to what
238+
the anonymous user can see in the web interface.
239+
240+
All 204 responses will include the headers:
235241

236242
* `Access-Control-Allow-Origin`
237243
* `Access-Control-Allow-Headers`
238244
* `Access-Control-Allow-Methods`
239-
* `Access-Control-Allow-Credentials: true`
240245
* `Access-Control-Max-Age: 86400`
241246

247+
If the client's ORIGIN header matches an entry besides ``*`` in the
248+
``allowed_api_origins`` it will also include:
249+
250+
* `Access-Control-Allow-Credentials: true`
251+
252+
permitting the client to log in and perform authenticated operations.
253+
242254
If the endpoint accepts the PATCH verb the header `Accept-Patch` with
243255
valid mime types (usually `application/x-www-form-urlencoded,
244256
multipart/form-data`) will be included.

doc/upgrading.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,25 @@ are not used.)
236236
For details on WAL mode see `<https://www.sqlite.org/wal.html>`_
237237
and `<https://www.sqlite.org/pragma.html#pragma_journal_mode>`_.
238238

239+
Change in processing allowed_api_origins setting
240+
------------------------------------------------
241+
242+
In this release you can use both ``*`` (as the first origin) and
243+
explicit origins in the `allowed_api_origins`` setting in
244+
``config.ini``. (Before it was only one or the other.)
245+
246+
You do not need to use ``*``. If you do, it allows any client
247+
anonymous (unauthenticated) access to the Roundup tracker. This
248+
is the same as browsing the tracker without logging in. If they
249+
try to provide credentials, access to the data will be denied by
250+
`CORS`_.
251+
252+
If you include explicit origins (e.g. \https://example.com),
253+
users from those origins will not be blocked if they use
254+
credentials to log in.
255+
256+
.. _CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
257+
239258
Change in processing of In-Reply_to email header
240259
------------------------------------------------
241260

roundup/cgi/client.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,15 +1279,20 @@ def check_anonymous_access(self):
12791279
raise Unauthorised(self._("Anonymous users are not "
12801280
"allowed to use the web interface"))
12811281

1282-
def is_origin_header_ok(self, api=False):
1282+
def is_origin_header_ok(self, api=False, credentials=False):
12831283
"""Determine if origin is valid for the context
12841284
1285-
Allow (return True) if ORIGIN is missing and it is a GET.
1286-
Allow if ORIGIN matches the base url.
1285+
Header is ok (return True) if ORIGIN is missing and it is a GET.
1286+
Header is ok if ORIGIN matches the base url.
12871287
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.
1288+
Header is ok if ORIGIN matches an element of allowed_api_origins.
1289+
Header is ok if allowed_api_origins includes '*' as first
1290+
element and credentials is False.
1291+
Otherwise header is not ok.
1292+
1293+
In a credentials context, if we match * we will return
1294+
header is not ok. All credentialed requests must be
1295+
explicitly matched.
12911296
"""
12921297

12931298
try:
@@ -1312,9 +1317,15 @@ def is_origin_header_ok(self, api=False):
13121317
# Original spec says origin is case sensitive match.
13131318
# Living spec doesn't address Origin value's case or
13141319
# how to compare it. So implement case sensitive....
1315-
if allowed_origins:
1316-
if allowed_origins[0] == '*' or origin in allowed_origins:
1317-
return True
1320+
if origin in allowed_origins:
1321+
return True
1322+
# Block use of * when origin match is used for
1323+
# allowing credentials. See:
1324+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
1325+
# under Credentials Requests and Wildcards
1326+
if ( allowed_origins and allowed_origins[0] == '*'
1327+
and not credentials):
1328+
return True
13181329

13191330
return False
13201331

roundup/configuration.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,10 @@ def set(self, _val):
571571
pathlist = self._value = []
572572
for elem in _val.split():
573573
pathlist.append(elem)
574-
if '*' in pathlist and len(pathlist) != 1:
575-
raise OptionValueError(self, _val,
576-
"If using '*' it must be the only element.")
574+
if '*' in pathlist and pathlist[0] != '*':
575+
raise OptionValueError(
576+
self, _val,
577+
"If using '*' it must be the first element.")
577578

578579
def _value2str(self, value):
579580
return ','.join(value)
@@ -1317,13 +1318,13 @@ def str2value(self, value):
13171318
'https://Bar.edu' are two different Origin values. Note that
13181319
the origin value is scheme://host. There is no path
13191320
component. So 'https://bar.edu/' would never be valid.
1320-
Also the value * can be used to match any origin. Note that
1321-
this value allows any web page on the internet to make
1322-
authenticated requests against your Roundup tracker and
1323-
is not a good idea.
1321+
The value '*' can be used to match any origin. It must be
1322+
first in the list if used. Note that this value allows
1323+
any web page on the internet to make anonymous requests
1324+
against your Roundup tracker.
13241325
13251326
You need to set these if you have a web application on a
1326-
different origin accessing your roundup instance.
1327+
different origin accessing your Roundup instance.
13271328
13281329
(The origin from the tracker.web setting in config.ini is
13291330
always valid and does not need to be specified.)"""),

roundup/rest.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,11 +2201,24 @@ def dispatch(self, method, uri, input):
22012201
self.client.request.headers.get("Origin")
22022202
)
22032203

2204-
# allow credentials
2205-
self.client.setHeader(
2206-
"Access-Control-Allow-Credentials",
2207-
"true"
2208-
)
2204+
# Allow credentials if origin is acceptable.
2205+
#
2206+
# If Access-Control-Allow-Credentials header not returned,
2207+
# but the client request is made with credentials
2208+
# data will be sent but not made available to the
2209+
# calling javascript in browser.
2210+
# Prevents exposure of data to an invalid origin when
2211+
# credentials are sent by client.
2212+
#
2213+
# If admin puts * first in allowed_api_origins
2214+
# we do not allow credentials but do reflect the origin.
2215+
# This allows anonymous access.
2216+
if self.client.is_origin_header_ok(api=True, credentials=True):
2217+
self.client.setHeader(
2218+
"Access-Control-Allow-Credentials",
2219+
"true"
2220+
)
2221+
22092222
# set allow header in case of error. 405 handlers below should
22102223
# replace it with a custom version as will OPTIONS handler
22112224
# doing CORS.

0 commit comments

Comments
 (0)