8000 Use constant-time comparison for passwords. · python-websockets/websockets@547a26b · GitHub
[go: up one dir, main page]

Skip to content

Commit 547a26b

Browse files
committed
Use constant-time comparison for passwords.
Backport of c91b4c2 and dfecbd0.
1 parent a14226a commit 547a26b

File tree

3 files changed

+30
-15
lines changed

3 files changed

+30
-15
lines changed

docs/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ They may change at any time.
3030

3131
*In development*
3232

33+
.. note::
34+
35+
**Version 9.1 fixes a security issue introduced in version 8.0.**
36+
37+
Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords.
38+
3339
9.0.2
3440
.....
3541

src/websockets/legacy/auth.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
import functools
9+
import hmac
910
import http
1011
from typing import Any, Awaitable, Callable, Iterable, Optional, Tuple, Union, cast
1112

@@ -132,24 +133,23 @@ def basic_auth_protocol_factory(
132133

133134
if credentials is not None:
134135
if is_credentials(credentials):
135-
136-
async def check_credentials(username: str, password: str) -> bool:
137-
return (username, password) == credentials
138-
136+
credentials_list = [cast(Credentials, credentials)]
139137
elif isinstance(credentials, Iterable):
140138
credentials_list = list(credentials)
141-
if all(is_credentials(item) for item in credentials_list):
142-
credentials_dict = dict(credentials_list)
143-
144-
async def check_credentials(username: str, password: str) -> bool:
145-
return credentials_dict.get(username) == password
146-
147-
else:
139+
if not all(is_credentials(item) for item in credentials_list):
148140
raise TypeError(f"invalid credentials argument: {credentials}")
149-
150141
else:
151142
raise TypeError(f"invalid credentials argument: {credentials}")
152143

144+
credentials_dict = dict(credentials_list)
145+
146+
async def check_credentials(username: str, password: str) -> bool:
147+
try:
148+
expected_password = credentials_dict[username]
149+
except KeyError:
150+
return False
151+
return hmac.compare_digest(expected_password, password)
152+
153153
if create_protocol is None:
154154
# Not sure why mypy cannot figure this out.
155155
create_protocol = cast(
@@ -158,5 +158,7 @@ async def check_credentials(username: str, password: str) -> bool:
158158
)
159159

160160
return functools.partial(
161-
create_protocol, realm=realm, check_credentials=check_credentials
161+
create_protocol,
162+
realm=realm,
163+
check_credentials=check_credentials,
162164
)

tests/legacy/test_auth.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hmac
12
import unittest
23
import urllib.error
34

@@ -76,7 +77,7 @@ def test_basic_auth_bad_multiple_credentials(self):
7677
)
7778

7879
async def check_credentials(username, password):
79-
return password == "iloveyou"
80+
return hmac.compare_digest(password, "iloveyou")
8081

8182
create_protocol_check_credentials = basic_auth_protocol_factory(
8283
realm="auth-tests",
@@ -140,7 +141,13 @@ def test_basic_auth_unsupported_credentials_details(self):
140141
self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n")
141142

142143
@with_server(create_protocol=create_protocol)
143-
def test_basic_auth_invalid_credentials(self):
144+
def test_basic_auth_invalid_username(self):
145+
with self.assertRaises(InvalidStatusCode) as raised:
146+
self.start_client(user_info=("goodbye", "iloveyou"))
147+
self.assertEqual(raised.exception.status_code, 401)
148+
149+
@with_server(create_protocol=create_protocol)
150+
def test_basic_auth_invalid_password(self):
144151
with self.assertRaises(InvalidStatusCode) as raised:
145152
self.start_client(user_info=("hello", "ihateyou"))
146153
self.assertEqual(raised.exception.status_code, 401)

0 commit comments

Comments
 (0)
0