Skip to content

Commit ad41702

Browse files
dkruppbruntib
authored andcommitted
Fix the endpoint parsing issue
URL parsing is hardened in the web server.
1 parent 866f379 commit ad41702

File tree

10 files changed

+105
-63
lines changed

10 files changed

+105
-63
lines changed

web/api/products.thrift

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ service codeCheckerProductService {
7474

7575
// Get the list of product that matches the display name and endpoint
7676
// filters specified.
77+
// PERMISSION: PRODUCT_VIEW
7778
Products getProducts(1: string productEndpointFilter,
7879
2: string productNameFilter)
7980
throws (1: codechecker_api_shared.RequestFailed requestError),

web/api/report_server.thrift

+2-1
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ service codeCheckerDBAccess {
688688
throws (1: codechecker_api_shared.RequestFailed requestError),
689689

690690
// Return true if review status change is disabled.
691-
// PERMISSION: PRODUCT_ACCESS or PRODUCT_STORE
691+
// PERMISSION: PRODUCT_VIEW
692692
bool isReviewStatusChangeDisabled()
693693
throws (1: codechecker_api_shared.RequestFailed requestError),
694694

@@ -769,6 +769,7 @@ service codeCheckerDBAccess {
769769
// get the md documentation for a checker
770770
// DEPRECATED. Use getCheckerLabels() instead which contains checker
771771
// documentation URL.
772+
// PERMISSION: PRODUCT_VIEW
772773
string getCheckerDoc(1: string checkerId)
773774
throws (1: codechecker_api_shared.RequestFailed requestError),
774775

web/server/codechecker_server/api/config_handler.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,23 @@ class ThriftConfigHandler:
3131
Manages Thrift requests regarding configuration.
3232
"""
3333

34-
def __init__(self, auth_session, config_session):
34+
def __init__(self, auth_session, config_session, session_manager):
3535
self.__auth_session = auth_session
3636
self.__session = config_session
37+
self.__session_manager = session_manager
3738

3839
def __require_supermission(self):
3940
"""
4041
Checks if the current user isn't a SUPERUSER.
4142
"""
43+
44+
# Anonymous access is only allowed if authentication is
45+
# turned off
46+
if self.__session_manager.is_enabled and not self.__auth_session:
47+
raise codechecker_api_shared.ttypes.RequestFailed(
48+
codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
49+
"You are not authorized to execute this action.")
50+
4251
if (not (self.__auth_session is None) and
4352
not self.__auth_session.is_root):
4453
raise codechecker_api_shared.ttypes.RequestFailed(
@@ -69,7 +78,7 @@ def getNotificationBannerText(self):
6978
def setNotificationBannerText(self, notification_b64):
7079
"""
7180
Sets the notification banner remove_products_except.
72-
Bevare: This method only works if the use is a SUPERUSER.
81+
Beware: This method only works if the use is a SUPERUSER.
7382
"""
7483

7584
self.__require_supermission()

web/server/codechecker_server/api/product_server.py

+8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def __require_permission(self, required, args=None):
6969
args = dict(self.__permission_args)
7070
args['config_db_session'] = session
7171

72+
# Anonymous access is only allowed if authentication is
73+
# turned off
74+
if self.__server.manager.is_enabled and not self.__auth_session:
75+
raise codechecker_api_shared.ttypes.RequestFailed(
76+
codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
77+
"You are not authorized to execute this action.")
78+
7279
if not any(permissions.require_permission(
7380
perm, args, self.__auth_session)
7481
for perm in required):
@@ -247,6 +254,7 @@ def getProductConfiguration(self, product_id):
247254
Get the product configuration --- WITHOUT THE DB PASSWORD --- of the
248255
given product.
249256
"""
257+
self.__require_permission([permissions.PRODUCT_VIEW])
250258

251259
with DBSession(self.__session) as session:
252260
product = session.query(Product).get(product_id)

web/server/codechecker_server/api/report_server.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,13 @@ def __require_permission(self, required):
14471447
args = dict(self.__permission_args)
14481448
args['config_db_session'] = session
14491449

1450+
# Anonymous access is only allowed if authentication is
1451+
# turned off
1452+
if self._manager.is_enabled and not self._auth_session:
1453+
raise codechecker_api_shared.ttypes.RequestFailed(
1454+
codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
1455+
"You are not authorized to execute this action.")
1456+
14501457
if not any(permissions.require_permission(
14511458
perm, args, self._auth_session)
14521459
for perm in required):
@@ -2320,6 +2327,7 @@ def _setReviewStatus(self, session, report_hash, status,
23202327
database transaction. This is needed because during storage a specific
23212328
session object has to be used.
23222329
"""
2330+
23232331
review_status = session.query(ReviewStatus).get(report_hash)
23242332
if review_status is None:
23252333
review_status = ReviewStatus()
@@ -2421,6 +2429,8 @@ def isReviewStatusChangeDisabled(self):
24212429
"""
24222430
Return True if review status change is disabled.
24232431
"""
2432+
self.__require_view()
2433+
24242434
with DBSession(self._config_database) as session:
24252435
product = session.query(Product).get(self._product.id)
24262436
return product.is_review_status_change_disabled
@@ -2746,7 +2756,7 @@ def getCheckerDoc(self, _):
27462756
Parameters:
27472757
- checkerId
27482758
"""
2749-
2759+
self.__require_view()
27502760
return ""
27512761

27522762
@exc_to_thrift_reqfail
@@ -2756,6 +2766,8 @@ def getCheckerLabels(
27562766
checkers: List[ttypes.Checker]
27572767
) -> List[List[str]]:
27582768
""" Return the list of labels to each checker. """
2769+
self.__require_view()
2770+
27592771
labels = []
27602772
for checker in checkers:
27612773
analyzer_name = None if not checker.analyzerName \
@@ -3569,6 +3581,8 @@ def getFailedFilesCount(self, run_ids):
35693581
given run. If the run id list is empty the number of failed files will
35703582
be counted for all of the runs.
35713583
"""
3584+
self.__require_view()
3585+
35723586
# Unfortunately we can't distinct the failed file paths by using SQL
35733587
# queries because the list of failed files for a run / analyzer are
35743588
# stored in one column in a compressed way. For this reason we need to
@@ -3611,6 +3625,8 @@ def getFailedFiles(self, run_ids):
36113625
# -----------------------------------------------------------------------
36123626
@timeit
36133627
def getPackageVersion(self):
3628+
self.__require_view()
3629+
36143630
return self.__package_version
36153631

36163632
# -----------------------------------------------------------------------

web/server/codechecker_server/routing.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def is_supported_version(version):
7171

7272
version = version.lstrip('v')
7373
version_parts = version.split('.')
74+
if len(version_parts) < 2:
75+
return False
7476

7577
# We don't care if accidentally the version tag contains a revision number.
7678
major, minor = int(version_parts[0]), int(version_parts[1])
@@ -115,24 +117,27 @@ def split_client_POST_request(path):
115117
"""
116118

117119
# A standard POST request from an API client looks like:
118-
# http://localhost:8001/[product-name]/<API version>/<API service>
120+
# http://localhost:8001/[product-name/]<API version>/<API service>
119121
# where specifying the product name is optional.
120122

121123
split_path = urlparse(path).path.split('/', 3)
122124

123125
endpoint_part = split_path[1]
124-
if is_valid_product_endpoint(split_path[1]):
126+
if is_valid_product_endpoint(split_path[1]) and len(split_path) == 4:
125127
version_tag = split_path[2].lstrip('v')
126-
remainder = split_path[3]
128+
if not is_supported_version(version_tag):
129+
return None, None, None
130+
endpoint = split_path[3]
131+
return endpoint_part, version_tag, endpoint
127132

128-
return endpoint_part, version_tag, remainder
129-
elif split_path[1].startswith('v'):
133+
elif split_path[1].startswith('v') and len(split_path) == 3:
130134
# Request came through without a valid product URL endpoint to
131135
# possibly the main server.
132136
version_tag = split_path[1].lstrip('v')
133-
remainder = split_path[2]
134-
135-
return None, version_tag, remainder
137+
if not is_supported_version(version_tag):
138+
return None, None, None
139+
endpoint = split_path[2]
140+
return None, version_tag, endpoint
136141

137142
return None, None, None
138143

web/server/codechecker_server/server.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,21 @@ def do_POST(self):
328328
otrans = TTransport.TMemoryBuffer()
329329
oprot = output_protocol_factory.getProtocol(otrans)
330330

331+
product_endpoint, api_ver, request_endpoint = \
332+
routing.split_client_POST_request(self.path)
333+
334+
if product_endpoint is None and api_ver is None and \
335+
request_endpoint is None:
336+
self.send_thrift_exception("Invalid request endpoint path.", iprot,
337+
oprot, otrans)
338+
return
339+
340+
# Only Authentication, Configuration, ServerInof
341+
# endpoints are allowed for Anonymous users
342+
# if authentication is required.
331343
if self.server.manager.is_enabled and \
332-
not self.path.endswith(('/Authentication',
333-
'/Configuration',
334-
'/ServerInfo')) and \
344+
request_endpoint not in \
345+
['Authentication', 'Configuration', 'ServerInfo'] and \
335346
not self.auth_session:
336347
# Bail out if the user is not authenticated...
337348
# This response has the possibility of melting down Thrift clients,
@@ -347,12 +358,6 @@ def do_POST(self):
347358

348359
# Authentication is handled, we may now respond to the user.
349360
try:
350-
product_endpoint, api_ver, request_endpoint = \
351-
routing.split_client_POST_request(self.path)
352-
if product_endpoint is None and api_ver is None and \
353-
request_endpoint is None:
354-
raise ValueError("Invalid request endpoint path.")
355-
356361
product = None
357362
if product_endpoint:
358363
# The current request came through a product route, and not
@@ -373,7 +378,8 @@ def do_POST(self):
373378
elif request_endpoint == 'Configuration':
374379
conf_handler = ConfigHandler_v6(
375380
self.auth_session,
376-
self.server.config_session)
381+
self.server.config_session,
382+
self.server.manager)
377383
processor = ConfigAPI_v6.Processor(conf_handler)
378384
elif request_endpoint == 'ServerInfo':
379385
server_info_handler = ServerInfoHandler_v6(version)

web/server/tests/unit/test_request_routing.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,12 @@ def test_post(self):
5252
# It is the server code's responsibility to give a 404 Not Found.
5353
self.assertEqual(post(''), (None, None, None))
5454
self.assertEqual(post('CodeCheckerService'), (None, None, None))
55-
56-
# Raise an exception if URL is malformed, such as contains a
57-
# product-endpoint-like component which is badly encoded version
58-
# string.
59-
with self.assertRaises(Exception):
60-
post('v6.0')
61-
post('/v6/CodeCheckerService')
55+
self.assertEqual(post('v6.0'), (None, None, None))
56+
self.assertEqual(post('/v6.0/product/Authentication/Service'),
57+
(None, None, None))
6258

6359
self.assertEqual(post('/v6.0/Authentication'),
6460
(None, '6.0', 'Authentication'))
6561

66-
self.assertEqual(post('/DummyProduct/v0.0/FoobarService'),
67-
('DummyProduct', '0.0', 'FoobarService'))
62+
self.assertEqual(post('/DummyProduct/v6.0/FoobarService'),
63+
('DummyProduct', '6.0', 'FoobarService'))

web/tests/functional/products/test_products.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def test_get_product_data(self):
150150

151151
# Now get the SERVERSPACE (configuration) for the product.
152152
# TODO: These things usually should only work for superusers!
153-
pr_conf = self._pr_client.getProductConfiguration(pr_data.id)
153+
pr_conf = self._root_client.getProductConfiguration(pr_data.id)
154154

155155
self.assertIsNotNone(pr_conf, "Product configuration must come.")
156156
self.assertEqual(pr_conf.endpoint, self.product_name,
@@ -189,7 +189,7 @@ def test_editing(self):
189189
pr_client = env.setup_product_client(
190190
self.test_workspace, product=self.product_name)
191191
product_id = pr_client.getCurrentProduct().id
192-
config = self._pr_client.getProductConfiguration(product_id)
192+
config = self._root_client.getProductConfiguration(product_id)
193193

194194
old_name = config.displayedName_b64
195195

@@ -202,7 +202,7 @@ def test_editing(self):
202202
self.assertTrue(self._root_client.editProduct(product_id, config),
203203
"Product edit didn't conclude.")
204204

205-
config = self._pr_client.getProductConfiguration(product_id)
205+
config = self._root_client.getProductConfiguration(product_id)
206206
self.assertEqual(config.endpoint, self.product_name,
207207
"The product edit changed the endpoint, when it "
208208
"shouldn't have!")
@@ -214,7 +214,7 @@ def test_editing(self):
214214
self.assertTrue(self._root_client.editProduct(product_id, config),
215215
"Product config restore didn't conclude.")
216216

217-
config = self._pr_client.getProductConfiguration(product_id)
217+
config = self._root_client.getProductConfiguration(product_id)
218218
self.assertEqual(config.displayedName_b64, old_name,
219219
"The product edit didn't change the name back.")
220220

@@ -225,7 +225,7 @@ def test_editing(self):
225225
self.assertTrue(self._root_client.editProduct(product_id, config),
226226
"Product edit didn't conclude.")
227227

228-
config = self._pr_client.getProductConfiguration(product_id)
228+
config = self._root_client.getProductConfiguration(product_id)
229229
self.assertEqual(config.confidentiality,
230230
new_confidentiality,
231231
"Couldn't change the confidentiality to OPEN")
@@ -235,7 +235,7 @@ def test_editing(self):
235235
self.assertTrue(self._root_client.editProduct(product_id, config),
236236
"Product edit didn't conclude.")
237237

238-
config = self._pr_client.getProductConfiguration(product_id)
238+
config = self._root_client.getProductConfiguration(product_id)
239239
self.assertEqual(config.confidentiality,
240240
new_confidentiality,
241241
"Couldn't change the confidentiality to INTERNAL")
@@ -245,7 +245,7 @@ def test_editing(self):
245245
self.assertTrue(self._root_client.editProduct(product_id, config),
246246
"Product edit didn't conclude.")
247247

248-
config = self._pr_client.getProductConfiguration(product_id)
248+
config = self._root_client.getProductConfiguration(product_id)
249249
self.assertEqual(config.confidentiality,
250250
new_confidentiality,
251251
"Couldn't change the confidentiality to CONFIDENTIAL")
@@ -255,7 +255,7 @@ def test_editing(self):
255255
self.assertTrue(self._root_client.editProduct(product_id, config),
256256
"Product config restore didn't conclude.")
257257

258-
config = self._pr_client.getProductConfiguration(product_id)
258+
config = self._root_client.getProductConfiguration(product_id)
259259
self.assertEqual(config.confidentiality,
260260
old_confidentiality,
261261
"The edit didn't change back the confidentiality.")
@@ -271,7 +271,7 @@ def test_editing_reconnect(self):
271271
pr_client = env.setup_product_client(
272272
self.test_workspace, product=self.product_name)
273273
product_id = pr_client.getCurrentProduct().id
274-
config = self._pr_client.getProductConfiguration(product_id)
274+
config = self._root_client.getProductConfiguration(product_id)
275275

276276
old_db_name = config.connection.database
277277

@@ -292,7 +292,7 @@ def test_editing_reconnect(self):
292292
"Product edit didn't conclude.")
293293

294294
# Check if the configuration now uses the new values.
295-
config = self._pr_client.getProductConfiguration(product_id)
295+
config = self._root_client.getProductConfiguration(product_id)
296296

297297
self.assertEqual(config.connection.database, new_db_name,
298298
"Server didn't save new database name.")
@@ -311,7 +311,7 @@ def test_editing_reconnect(self):
311311
self.assertTrue(self._root_client.editProduct(product_id, config),
312312
"Product configuration restore didn't conclude.")
313313

314-
config = self._pr_client.getProductConfiguration(product_id)
314+
config = self._root_client.getProductConfiguration(product_id)
315315
self.assertEqual(config.connection.database, old_db_name,
316316
"Server didn't save back to old database name.")
317317

@@ -336,7 +336,7 @@ def test_editing_endpoint(self):
336336
pr_client = env.setup_product_client(
337337
self.test_workspace, product=self.product_name)
338338
product_id = pr_client.getCurrentProduct().id
339-
config = self._pr_client.getProductConfiguration(product_id)
339+
config = self._root_client.getProductConfiguration(product_id)
340340

341341
old_endpoint = config.endpoint
342342
new_endpoint = "edited_endpoint"
@@ -347,7 +347,7 @@ def test_editing_endpoint(self):
347347
"Product edit didn't conclude.")
348348

349349
# Check if the configuration now uses the new values.
350-
config = self._pr_client.getProductConfiguration(product_id)
350+
config = self._root_client.getProductConfiguration(product_id)
351351
self.assertEqual(config.endpoint, new_endpoint,
352352
"Server didn't save new endpoint.")
353353

@@ -372,7 +372,7 @@ def test_editing_endpoint(self):
372372
self.assertTrue(self._root_client.editProduct(product_id, config),
373373
"Product configuration restore didn't conclude.")
374374

375-
config = self._pr_client.getProductConfiguration(product_id)
375+
config = self._root_client.getProductConfiguration(product_id)
376376
self.assertEqual(config.endpoint, old_endpoint,
377377
"Server didn't save back to old endpoint.")
378378

0 commit comments

Comments
 (0)