Skip to content

Commit 8549c24

Browse files
committed
Support arbitrary data in basic auth credentials.
1 parent a8a898b commit 8549c24

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎src/websockets/headers.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,8 @@ def parse_authorization_basic(header: str) -> tuple[str, str]:
530530
531531
Return a ``(username, password)`` tuple.
532532
533+
Non-UTF-8 data is represented with surrogate code points.
534+
533535
Args:
534536
header: Value of the ``Authorization`` header.
535537
@@ -555,12 +557,13 @@ def parse_authorization_basic(header: str) -> tuple[str, str]:
555557
parse_end(header, pos, "Authorization")
556558

557559
try:
558-
user_pass = base64.b64decode(basic_credentials.encode()).decode()
560+
user_pass_bytes = base64.b64decode(basic_credentials.encode())
559561
except binascii.Error:
560562
raise InvalidHeaderValue(
561563
"Authorization",
562564
"expected base64-encoded credentials",
563565
) from None
566+
user_pass = user_pass_bytes.decode(errors="surrogateescape")
564567
try:
565568
username, password = user_pass.split(":", 1)
566569
except ValueError:
@@ -578,9 +581,12 @@ def build_authorization_basic(username: str, password: str) -> str:
578581
579582
This is the reverse of :func:`parse_authorization_basic`.
580583
584+
Non-UTF-8 data can be represented with surrogate code points.
585+
581586
"""
582587
# https://datatracker.ietf.org/doc/html/rfc7617#section-2
583588
assert ":" not in username
584589
user_pass = f"{username}:{password}"
585-
basic_credentials = base64.b64encode(user_pass.encode()).decode()
590+
user_pass_bytes = user_pass.encode(errors="surrogateescape")
591+
basic_credentials = base64.b64encode(user_pass_bytes).decode()
586592
return "Basic " + basic_credentials

‎tests/asyncio/test_server.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,19 @@ async def test_valid_authorization_non_ascii_password(self):
852852
) as client:
853853
await self.assertEval(client, "ws.username", "test")
854854

855+
async def test_valid_authorization_non_utf8_password(self):
856+
"""basic_auth authenticates client with non-UTF-8 password."""
857+
password = b"\xca\xfe".decode(errors="surrogateescape")
858+
async with serve(
859+
*args,
860+
process_request=basic_auth(credentials=("drink", password)),
861+
) as server:
862+
async with connect(
863+
get_uri(server),
864+
additional_headers={"Authorization": "Basic ZHJpbms6yv4="},
865+
) as client:
866+
await self.assertEval(client, "ws.username", "drink")
867+
855868
async def test_missing_authorization(self):
856869
"""basic_auth rejects client without credentials."""
857870
async with serve(

‎tests/sync/test_server.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,18 @@ def test_valid_authorization_non_ascii_password(self):
635635
) as client:
636636
self.assertEval(client, "ws.username", "test")
637637

638+
def test_valid_authorization_non_utf8_password(self):
639+
"""basic_auth authenticates client with non-UTF-8 password."""
640+
password = b"\xca\xfe".decode(errors="surrogateescape")
641+
with run_server(
642+
process_request=basic_auth(credentials=("drink", password)),
643+
) as server:
644+
with connect(
645+
get_uri(server),
646+
additional_headers={"Authorization": "Basic ZHJpbms6yv4="},
647+
) as client:
648+
self.assertEval(client, "ws.username", "drink")
649+
638650
def test_missing_authorization(self):
639651
"""basic_auth rejects client without credentials."""
640652
with run_server(

0 commit comments

Comments
 (0)