Mercurial > p > roundup > code
comparison test/test_cgi.py @ 7155:89a59e46b3af
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.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 23 Feb 2023 12:01:33 -0500 |
| parents | f614176903d0 |
| children | ed63b6d35838 |
comparison
equal
deleted
inserted
replaced
| 7154:f614176903d0 | 7155:89a59e46b3af |
|---|---|
| 1188 # clean up from email log | 1188 # clean up from email log |
| 1189 if os.path.exists(SENDMAILDEBUG): | 1189 if os.path.exists(SENDMAILDEBUG): |
| 1190 os.remove(SENDMAILDEBUG) | 1190 os.remove(SENDMAILDEBUG) |
| 1191 #raise ValueError | 1191 #raise ValueError |
| 1192 | 1192 |
| 1193 @pytest.mark.xfail | 1193 def testRestOriginValidationCredentials(self): |
| 1194 def testRestOriginValidation(self): | |
| 1195 import json | 1194 import json |
| 1196 # set the password for admin so we can log in. | 1195 # set the password for admin so we can log in. |
| 1197 passwd=password.Password('admin') | 1196 passwd=password.Password('admin') |
| 1198 self.db.user.set('1', password=passwd) | 1197 self.db.user.set('1', password=passwd) |
| 1199 | 1198 |
| 1248 cl.additional_headers['Access-Control-Allow-Origin'], | 1247 cl.additional_headers['Access-Control-Allow-Origin'], |
| 1249 'http://whoami.com' | 1248 'http://whoami.com' |
| 1250 ) | 1249 ) |
| 1251 del(out[0]) | 1250 del(out[0]) |
| 1252 | 1251 |
| 1252 | |
| 1253 # Origin not set. AKA same origin GET request. | |
| 1254 # Should be like valid origin. | |
| 1255 # Because of HTTP_X_REQUESTED_WITH header it should be | |
| 1256 # preflighted. | |
| 1257 cl = client.Client(self.instance, None, | |
| 1258 {'REQUEST_METHOD':'GET', | |
| 1259 'PATH_INFO':'rest/data/issue', | |
| 1260 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', | |
| 1261 'HTTP_REFERER': 'http://whoami.com/path/', | |
| 1262 'HTTP_ACCEPT': "application/json;version=1", | |
| 1263 'HTTP_X_REQUESTED_WITH': 'rest', | |
| 1264 }, form) | |
| 1265 cl.db = self.db | |
| 1266 cl.base = 'http://whoami.com/path/' | |
| 1267 cl._socket_op = lambda *x : True | |
| 1268 cl._error_message = [] | |
| 1269 cl.request = MockNull() | |
| 1270 h = { 'content-type': 'application/json', | |
| 1271 'accept': 'application/json' } | |
| 1272 cl.request.headers = MockNull(**h) | |
| 1273 | |
| 1274 cl.write = wh # capture output | |
| 1275 | |
| 1276 # Should return explanation because content type is text/plain | |
| 1277 # and not text/xml | |
| 1278 cl.handle_rest() | |
| 1279 self.assertIn('Access-Control-Allow-Credentials', | |
| 1280 cl.additional_headers) | |
| 1281 | |
| 1282 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected)) | |
| 1283 del(out[0]) | |
| 1284 | |
| 1285 cl = client.Client(self.instance, None, | |
| 1286 {'REQUEST_METHOD':'OPTIONS', | |
| 1287 'HTTP_ORIGIN': 'http://invalid.com', | |
| 1288 'PATH_INFO':'rest/data/issue', | |
| 1289 'Access-Control-Request-Headers': 'Authorization', | |
| 1290 'Access-Control-Request-Method': 'GET', | |
| 1291 }, form) | |
| 1292 cl.db = self.db | |
| 1293 cl.base = 'http://whoami.com/path/' | |
| 1294 cl._socket_op = lambda *x : True | |
| 1295 cl._error_message = [] | |
| 1296 cl.request = MockNull() | |
| 1297 h = { 'content-type': 'application/json', | |
| 1298 'accept': 'application/json', | |
| 1299 'access-control-request-headers': 'Authorization', | |
| 1300 'access-control-request-method': 'GET', | |
| 1301 } | |
| 1302 cl.request.headers = MockNull(**h) | |
| 1303 | |
| 1304 cl.write = wh # capture output | |
| 1305 | |
| 1306 # Should return explanation because content type is text/plain | |
| 1307 # and not text/xml | |
| 1308 cl.handle_rest() | |
| 1309 self.assertNotIn('Access-Control-Allow-Credentials', | |
| 1310 cl.additional_headers) | |
| 1311 | |
| 1312 self.assertNotIn('Access-Control-Allow-Origin', | |
| 1313 cl.additional_headers | |
| 1314 ) | |
| 1315 | |
| 1316 self.assertEqual(cl.response_code, 400) | |
| 1317 del(out[0]) | |
| 1253 | 1318 |
| 1254 # origin not set to allowed value | 1319 # origin not set to allowed value |
| 1255 # prevents authenticated request like this from | 1320 # prevents authenticated request like this from |
| 1256 # being shared with the requestor because | 1321 # being shared with the requestor because |
| 1257 # Access-Control-Allow-Credentials is not | 1322 # Access-Control-Allow-Credentials is not |
| 1281 cl.write = wh # capture output | 1346 cl.write = wh # capture output |
| 1282 cl.handle_rest() | 1347 cl.handle_rest() |
| 1283 self.assertEqual(json.loads(b2s(out[0])), | 1348 self.assertEqual(json.loads(b2s(out[0])), |
| 1284 json.loads(expected) | 1349 json.loads(expected) |
| 1285 ) | 1350 ) |
| 1286 self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers) | 1351 self.assertNotIn('Access-Control-Allow-Credentials', |
| 1352 cl.additional_headers) | |
| 1353 self.assertIn('Access-Control-Allow-Origin', | |
| 1354 cl.additional_headers) | |
| 1355 self.assertEqual( | |
| 1356 h['origin'], | |
| 1357 cl.additional_headers['Access-Control-Allow-Origin'] | |
| 1358 ) | |
| 1359 | |
| 1287 self.assertIn('Content-Length', cl.additional_headers) | 1360 self.assertIn('Content-Length', cl.additional_headers) |
| 1288 del(out[0]) | 1361 del(out[0]) |
| 1289 | 1362 |
| 1290 | 1363 |
| 1291 # origin not set. Same rules as for invalid origin | 1364 # CORS Same rules as for invalid origin |
| 1292 cl = client.Client(self.instance, None, | 1365 cl = client.Client(self.instance, None, |
| 1293 {'REQUEST_METHOD':'GET', | 1366 {'REQUEST_METHOD':'GET', |
| 1294 'PATH_INFO':'rest/data/issue', | 1367 'PATH_INFO':'rest/data/issue', |
| 1295 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', | 1368 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', |
| 1296 'HTTP_REFERER': 'http://whoami.com/path/', | 1369 'HTTP_REFERER': 'http://whoami.com/path/', |
| 1309 cl.write = wh # capture output | 1382 cl.write = wh # capture output |
| 1310 | 1383 |
| 1311 # Should return explanation because content type is text/plain | 1384 # Should return explanation because content type is text/plain |
| 1312 # and not text/xml | 1385 # and not text/xml |
| 1313 cl.handle_rest() | 1386 cl.handle_rest() |
| 1314 self.assertNotIn('Access-Control-Allow-Credentials', cl.additional_headers) | 1387 self.assertIn('Access-Control-Allow-Credentials', |
| 1388 cl.additional_headers) | |
| 1315 | 1389 |
| 1316 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected)) | 1390 self.assertEqual(json.loads(b2s(out[0])),json.loads(expected)) |
| 1317 del(out[0]) | 1391 del(out[0]) |
| 1318 | 1392 |
| 1319 # origin set to special "null" value. Same rules as for invalid origin | 1393 # origin set to special "null" value. Same rules as for |
| 1394 # invalid origin | |
| 1320 cl = client.Client(self.instance, None, | 1395 cl = client.Client(self.instance, None, |
| 1321 {'REQUEST_METHOD':'GET', | 1396 {'REQUEST_METHOD':'GET', |
| 1322 'PATH_INFO':'rest/data/issue', | 1397 'PATH_INFO':'rest/data/issue', |
| 1323 'ORIGIN': 'null', | 1398 'HTTP_ORIGIN': 'null', |
| 1324 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', | 1399 'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', |
| 1325 'HTTP_REFERER': 'http://whoami.com/path/', | 1400 'HTTP_REFERER': 'http://whoami.com/path/', |
| 1326 'HTTP_ACCEPT': "application/json;version=1", | 1401 'HTTP_ACCEPT': "application/json;version=1", |
| 1327 'HTTP_X_REQUESTED_WITH': 'rest', | 1402 'HTTP_X_REQUESTED_WITH': 'rest', |
| 1328 }, form) | 1403 }, form) |
