diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index c98479741b..923aaa841d 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -2520,6 +2520,7 @@ class OrderCalendarExport(ICalFeed): https://www.djangosnippets.org/snippets/243/ """ import base64 + import binascii if request.user.is_authenticated: # Authenticated on first try - maybe normal browser call? @@ -2534,11 +2535,20 @@ class OrderCalendarExport(ICalFeed): # NOTE: We are only support basic authentication for now. # if auth[0].lower() == 'basic': - uname, passwd = base64.b64decode(auth[1]).decode('ascii').split(':') - user = authenticate(username=uname, password=passwd) - if user is not None and user.is_active: - login(request, user) - request.user = user + try: + # Split on the first colon only - passwords may contain ':' + uname, passwd = ( + base64.b64decode(auth[1]).decode('utf-8').split(':', 1) + ) + except (binascii.Error, UnicodeDecodeError, ValueError): + # Malformed header - fall through to the 401 response + uname = passwd = None + + if uname is not None: + user = authenticate(username=uname, password=passwd) + if user is not None and user.is_active: + login(request, user) + request.user = user # Check again if request.user.is_authenticated: diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index c40f35ef94..396c12d72c 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1005,6 +1005,39 @@ class PurchaseOrderTest(OrderTest): ) self.assertEqual(response.status_code, 200) + def test_po_calendar_auth_password_colon(self): + """Test header authorization with a password containing a colon.""" + password = 'pass:word:123' + self.user.set_password(password) + self.user.save() + self.client.logout() + + base64_token = base64.b64encode(f'{self.username}:{password}'.encode()).decode( + 'ascii' + ) + response = self.client.get( + reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}), + format='json', + headers={'authorization': f'basic {base64_token}'}, + ) + self.assertEqual(response.status_code, 200) + + def test_po_calendar_auth_malformed(self): + """Test that malformed basic authorization headers return 401, not 500.""" + self.client.logout() + url = reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}) + + for token in [ + 'not-base64!!', # invalid base64 + base64.b64encode(b'\xff\xfe\xfd').decode('ascii'), # invalid utf-8 + base64.b64encode(b'no-colon-here').decode('ascii'), # no separator + ]: + response = self.client.get( + url, format='json', headers={'authorization': f'basic {token}'} + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"') + def test_po_calendar_no_permission(self): """Test that an authenticated user without purchase_order view permission is denied.""" self.clearRoles()