CVE-2021-33880 (published 2021-06-06T15:15Z)

The aaugustin websockets library before 9.1 for Python has an Observable Timing Discrepancy on servers when HTTP Basic Authentication is enabled with basic_auth_protocol_factory(credentials=...). An attacker may be able to guess a password via a timing attack.

Severity: LOW
CVSS v2 base score: 2.6 (AV:N/AC:H/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 5.9, MEDIUM (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-203
cwe_name: Observable Discrepancy
description: The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor, which exposes security-relevant information about the state of the product, such as whether a particular operation was successful or not.
extended_description: Discrepancies can take many forms, and variations may be detectable in timing, control flow, communications such as replies or requests, or general behavior. These discrepancies can reveal information about the product's operation or internal state to an unauthorized actor. In some cases, discrepancies can be used by attackers to form a side channel.
         

Fix information:
repo_url: https://github.com/aaugustin/websockets
commit_details: repo_url: https://github.com/aaugustin/websockets
author: Aymeric Augustin
author_date: 2021-05-23 18:51:27+02:00
msg: Use constant-time comparison for passwords.

Backport of c91b4c2a and dfecbd03.
repository: repo_url: https://github.com/aaugustin/websockets
repo_name: python-websockets/websockets
description: Library for building WebSocket servers and clients in Python
repo_language: Python
owner: aaugustin
file_changes: filename: changelog.rst
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,12 @@ They may change at any time.
 
 *In development*
 
+.. note::
+
+    **Version 9.1 fixes a security issue introduced in version 8.0.**
+
+    Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords.
+
 9.0.2
 .....
 

programming_language: Markdown
filename: auth.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@
 
 
 import functools
+import hmac
 import http
 from typing import Any, Awaitable, Callable, Iterable, Optional, Tuple, Union, cast
 
@@ -132,24 +133,23 @@ def basic_auth_protocol_factory(
 
     if credentials is not None:
         if is_credentials(credentials):
-
-            async def check_credentials(username: str, password: str) -> bool:
-                return (username, password) == credentials
-
+            credentials_list = [cast(Credentials, credentials)]
         elif isinstance(credentials, Iterable):
             credentials_list = list(credentials)
-            if all(is_credentials(item) for item in credentials_list):
-                credentials_dict = dict(credentials_list)
-
-                async def check_credentials(username: str, password: str) -> bool:
-                    return credentials_dict.get(username) == password
-
-            else:
+            if not all(is_credentials(item) for item in credentials_list):
                 raise TypeError(f"invalid credentials argument: {credentials}")
-
         else:
             raise TypeError(f"invalid credentials argument: {credentials}")
 
+        credentials_dict = dict(credentials_list)
+
+        async def check_credentials(username: str, password: str) -> bool:
+            try:
+                expected_password = credentials_dict[username]
+            except KeyError:
+                return False
+            return hmac.compare_digest(expected_password, password)
+
     if create_protocol is None:
         # Not sure why mypy cannot figure this out.
         create_protocol = cast(
@@ -158,5 +158,7 @@ def basic_auth_protocol_factory(
         )
 
     return functools.partial(
-        create_protocol, realm=realm, check_credentials=check_credentials
+        create_protocol,
+        realm=realm,
+        check_credentials=check_credentials,
     )

programming_language: Python
filename: test_auth.py
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,4 @@
+import hmac
 import unittest
 import urllib.error
 
@@ -76,7 +77,7 @@ class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase):
         )
 
     async def check_credentials(username, password):
-        return password == "iloveyou"
+        return hmac.compare_digest(password, "iloveyou")
 
     create_protocol_check_credentials = basic_auth_protocol_factory(
         realm="auth-tests",
@@ -140,7 +141,13 @@ class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase):
         self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n")
 
     @with_server(create_protocol=create_protocol)
-    def test_basic_auth_invalid_credentials(self):
+    def test_basic_auth_invalid_username(self):
+        with self.assertRaises(InvalidStatusCode) as raised:
+            self.start_client(user_info=("goodbye", "iloveyou"))
+        self.assertEqual(raised.exception.status_code, 401)
+
+    @with_server(create_protocol=create_protocol)
+    def test_basic_auth_invalid_password(self):
         with self.assertRaises(InvalidStatusCode) as raised:
             self.start_client(user_info=("hello", "ihateyou"))
         self.assertEqual(raised.exception.status_code, 401)

programming_language: Python