Skip to content

Commit 80e0f97

Browse files
committed
Add password masking in __repr__
1 parent 7829b01 commit 80e0f97

4 files changed

Lines changed: 21 additions & 10 deletions

File tree

CHANGES/1629.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Mask the password component in ``yarl.URL.__repr__`` to prevent accidental credential exposure in logs and debug
2+
output -- by :user:`mbaas038` and :user:`jhbuhrman`.

tests/test_url.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ def test_url_is_not_str() -> None:
5757

5858

5959
def test_str() -> None:
60-
url = URL("http://example.com:8888/path/to?a=1&b=2")
61-
assert str(url) == "http://example.com:8888/path/to?a=1&b=2"
60+
url = URL("http://user:password@example.com:8888/path/to?a=1&b=2")
61+
assert str(url) == "http://user:password@example.com:8888/path/to?a=1&b=2"
6262

6363

6464
def test_repr() -> None:
65-
url = URL("http://example.com")
66-
assert "URL('http://example.com')" == repr(url)
65+
url = URL("http://user:password@example.com")
66+
assert "URL('http://user:********@example.com')" == repr(url)
6767

6868

6969
def test_origin() -> None:

yarl/_parse.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def make_netloc(
161161
host: str | None,
162162
port: int | None,
163163
encode: bool = False,
164+
mask_password: bool = False,
164165
) -> str:
165166
"""Make netloc from parts.
166167
@@ -176,6 +177,8 @@ def make_netloc(
176177
if user is None and password is None:
177178
return ret
178179
if password is not None:
180+
if mask_password:
181+
password = "********"
179182
if not user:
180183
user = ""
181184
elif encode:

yarl/_url.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -491,24 +491,30 @@ def build(
491491
def __init_subclass__(cls) -> NoReturn:
492492
raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden")
493493

494-
def __str__(self) -> str:
494+
def _make_string(self, mask_password: bool = False) -> str:
495495
if not self._path and self._netloc and (self._query or self._fragment):
496496
path = "/"
497497
else:
498498
path = self._path
499-
if (port := self.explicit_port) is not None and port == DEFAULT_PORTS.get(
500-
self._scheme
501-
):
499+
if (
500+
(port := self.explicit_port) is not None
501+
and port == DEFAULT_PORTS.get(self._scheme)
502+
) or mask_password:
502503
# port normalization - using None for default ports to remove from rendering
503504
# https://datatracker.ietf.org/doc/html/rfc3986.html#section-6.2.3
504505
host = self.host_subcomponent
505-
netloc = make_netloc(self.raw_user, self.raw_password, host, None)
506+
netloc = make_netloc(
507+
self.raw_user, self.raw_password, host, None, mask_password=True
508+
)
506509
else:
507510
netloc = self._netloc
508511
return unsplit_result(self._scheme, netloc, path, self._query, self._fragment)
509512

513+
def __str__(self) -> str:
514+
return self._make_string(mask_password=False)
515+
510516
def __repr__(self) -> str:
511-
return f"{self.__class__.__name__}('{str(self)}')"
517+
return f"{self.__class__.__name__}('{self._make_string(mask_password=True)}')"
512518

513519
def __bytes__(self) -> bytes:
514520
return str(self).encode("ascii")

0 commit comments

Comments
 (0)