Skip to content

Commit fb53495

Browse files
committed
Mask passwords in URLs when converting to a string
1 parent e70d0b0 commit fb53495

File tree

5 files changed

+39
-15
lines changed

5 files changed

+39
-15
lines changed

httpx/_urls.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -371,33 +371,36 @@ def __hash__(self) -> int:
371371
def __eq__(self, other: typing.Any) -> bool:
372372
return isinstance(other, (URL, str)) and str(self) == str(URL(other))
373373

374-
def __str__(self) -> str:
375-
return str(self._uri_reference)
376-
377-
def __repr__(self) -> str:
378-
scheme, userinfo, host, port, path, query, fragment = self._uri_reference
379-
380-
if ":" in userinfo:
381-
# Mask any password component.
382-
userinfo = f'{userinfo.split(":")[0]}:[secure]'
374+
def _get_url_string(self) -> str:
375+
scheme, _userinfo, host, port, path, query, fragment = self._uri_reference
376+
# Do not use the userinfo at all
377+
# Basic auth credentials are passed as a header and do not need to be in the URL
383378

384379
authority = "".join(
385380
[
386-
f"{userinfo}@" if userinfo else "",
387381
f"[{host}]" if ":" in host else host,
388382
f":{port}" if port is not None else "",
389383
]
390384
)
385+
391386
url = "".join(
392387
[
393-
f"{self.scheme}:" if scheme else "",
388+
f"{scheme}:" if scheme else "",
394389
f"//{authority}" if authority else "",
395390
path,
396391
f"?{query}" if query is not None else "",
397392
f"#{fragment}" if fragment is not None else "",
398393
]
399394
)
400395

396+
return url
397+
398+
def __str__(self) -> str:
399+
# Always use this URL string to avoid inadvertently leaking credentials
400+
return self._get_url_string()
401+
402+
def __repr__(self) -> str:
403+
url = self._get_url_string()
401404
return f"{self.__class__.__name__}({url!r})"
402405

403406
@property

tests/client/test_auth.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ async def test_auth_disable_per_request() -> None:
302302

303303
def test_auth_hidden_url() -> None:
304304
url = "http://example-username:[email protected]/"
305-
expected = "URL('http://example-username:[secure]@example.org/')"
305+
expected = "URL('http://example.org/')"
306306
assert url == httpx.URL(url)
307307
assert expected == repr(httpx.URL(url))
308308

tests/models/test_responses.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ def test_response_json():
8989

9090

9191
def test_raise_for_status():
92-
request = httpx.Request("GET", "https://example.org")
92+
# The credentials here should be removed from all exception strings below
93+
request = httpx.Request("GET", "https://user:[email protected]")
9394

9495
# 2xx status codes are not an error.
9596
response = httpx.Response(200, request=request)

tests/models/test_url.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ def test_url_copywith_authority_subcomponents():
591591
}
592592
url = httpx.URL("https://example.org")
593593
new = url.copy_with(**copy_with_kwargs)
594-
assert str(new) == "https://username:password@example.net:444"
594+
assert str(new) == "https://example.net:444"
595595

596596

597597
def test_url_copywith_netloc():
@@ -610,7 +610,7 @@ def test_url_copywith_userinfo_subcomponents():
610610
}
611611
url = httpx.URL("https://example.org")
612612
new = url.copy_with(**copy_with_kwargs)
613-
assert str(new) == "https://tom%40example.org:abc123%40%20%@example.org"
613+
assert str(new) == "https://example.org"
614614
assert new.username == "[email protected]"
615615
assert new.password == "abc123@ %"
616616
assert new.userinfo == b"tom%40example.org:abc123%40%20%"

tests/test_utils.py

+20
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,26 @@ def test_logging_request(server, caplog):
6565
]
6666

6767

68+
def test_logging_request_with_auth(server, caplog):
69+
caplog.set_level(logging.INFO)
70+
with httpx.Client() as client:
71+
credentials = {
72+
"username": "user",
73+
"password": "abc123%",
74+
}
75+
url = server.url.copy_with(**credentials)
76+
response = client.get(url)
77+
assert response.status_code == 200
78+
79+
assert caplog.record_tuples == [
80+
(
81+
"httpx",
82+
logging.INFO,
83+
'HTTP Request: GET http://127.0.0.1:8000/ "HTTP/1.1 200 OK"',
84+
)
85+
]
86+
87+
6888
def test_logging_redirect_chain(server, caplog):
6989
caplog.set_level(logging.INFO)
7090
with httpx.Client(follow_redirects=True) as client:

0 commit comments

Comments
 (0)