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)

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