Skip to content

Commit 60f4499

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

File tree

4 files changed

+39
-12
lines changed

4 files changed

+39
-12
lines changed

httpx/_urls.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,7 @@ 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:
374+
def _get_masked_url_string(self) -> str:
378375
scheme, userinfo, host, port, path, query, fragment = self._uri_reference
379376

380377
if ":" in userinfo:
@@ -388,16 +385,25 @@ def __repr__(self) -> str:
388385
f":{port}" if port is not None else "",
389386
]
390387
)
388+
391389
url = "".join(
392390
[
393-
f"{self.scheme}:" if scheme else "",
391+
f"{scheme}:" if scheme else "",
394392
f"//{authority}" if authority else "",
395393
path,
396394
f"?{query}" if query is not None else "",
397395
f"#{fragment}" if fragment is not None else "",
398396
]
399397
)
400398

399+
return url
400+
401+
def __str__(self) -> str:
402+
# Always use the masked URL string to avoid inadvertently leaking credentials
403+
return self._get_masked_url_string()
404+
405+
def __repr__(self) -> str:
406+
url = self._get_masked_url_string()
401407
return f"{self.__class__.__name__}({url!r})"
402408

403409
@property

tests/models/test_responses.py

+6-5
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 password here should be redacted 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)
@@ -101,7 +102,7 @@ def test_raise_for_status():
101102
with pytest.raises(httpx.HTTPStatusError) as exc_info:
102103
response.raise_for_status()
103104
assert str(exc_info.value) == (
104-
"Informational response '101 Switching Protocols' for url 'https://example.org'\n"
105+
"Informational response '101 Switching Protocols' for url 'https://user:[secure]@example.org'\n"
105106
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101"
106107
)
107108

@@ -112,7 +113,7 @@ def test_raise_for_status():
112113
with pytest.raises(httpx.HTTPStatusError) as exc_info:
113114
response.raise_for_status()
114115
assert str(exc_info.value) == (
115-
"Redirect response '303 See Other' for url 'https://example.org'\n"
116+
"Redirect response '303 See Other' for url 'https://user:[secure]@example.org'\n"
116117
"Redirect location: 'https://other.org'\n"
117118
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303"
118119
)
@@ -124,7 +125,7 @@ def test_raise_for_status():
124125
with pytest.raises(httpx.HTTPStatusError) as exc_info:
125126
response.raise_for_status()
126127
assert str(exc_info.value) == (
127-
"Client error '403 Forbidden' for url 'https://example.org'\n"
128+
"Client error '403 Forbidden' for url 'https://user:[secure]@example.org'\n"
128129
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
129130
)
130131

@@ -135,7 +136,7 @@ def test_raise_for_status():
135136
with pytest.raises(httpx.HTTPStatusError) as exc_info:
136137
response.raise_for_status()
137138
assert str(exc_info.value) == (
138-
"Server error '500 Internal Server Error' for url 'https://example.org'\n"
139+
"Server error '500 Internal Server Error' for url 'https://user:[secure]@example.org'\n"
139140
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500"
140141
)
141142

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://username:[secure]@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://tom%40example.org:[secure]@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://user:[secure]@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)