Skip to content

Commit 75de094

Browse files
authored
Merge pull request #1104 from ascheel/main
Sanitize URLs for logging/display purposes.
2 parents fb9fe50 + c512bbf commit 75de094

File tree

4 files changed

+59
-8
lines changed

4 files changed

+59
-8
lines changed

changelog/1104.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This change will remove sensitive output in URLs for private repositories require a login provided in the format of http(s)://<user>:<pass>@domain.com, replacing the sensitive text with a sanitized "*****:*****" to prevent these details from showing up in log files.

tests/test_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ def test_get_repository_config_missing(config_file):
150150
assert utils.get_repository_from_config(config_file, "pypi") == exp
151151

152152

153+
def test_get_repository_config_url_with_auth(config_file):
154+
repository_url = "https://user:[email protected]/pypi"
155+
exp = {
156+
"repository": "https://notexisting.python.org/pypi",
157+
"username": "user",
158+
"password": "pass",
159+
}
160+
assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp
161+
assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp
162+
163+
164+
@pytest.mark.parametrize(
165+
"input_url, expected_url",
166+
[
167+
("https://upload.pypi.org/legacy/", "https://upload.pypi.org/legacy/"),
168+
(
169+
"https://user:[email protected]/legacy/",
170+
"https://********@upload.pypi.org/legacy/",
171+
),
172+
],
173+
)
174+
def test_sanitize_url(input_url: str, expected_url: str) -> None:
175+
assert utils.sanitize_url(input_url) == expected_url
176+
177+
153178
@pytest.mark.parametrize(
154179
"repo_url, message",
155180
[

twine/commands/upload.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
189189
# Determine if the user has passed in pre-signed distributions or any attestations.
190190
uploads, signatures, attestations_by_dist = _split_inputs(dists)
191191

192-
print(f"Uploading distributions to {repository_url}")
192+
print(f"Uploading distributions to {utils.sanitize_url(repository_url)}")
193193

194194
packages_to_upload = [
195195
_make_package(
@@ -250,8 +250,8 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
250250
# redirects as well.
251251
if resp.is_redirect:
252252
raise exceptions.RedirectDetected.from_args(
253-
repository_url,
254-
resp.headers["location"],
253+
utils.sanitize_url(repository_url),
254+
utils.sanitize_url(resp.headers["location"]),
255255
)
256256

257257
if skip_upload(resp, upload_settings.skip_existing, package):

twine/utils.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ def get_config(path: str) -> Dict[str, RepositoryConfig]:
100100
return dict(config)
101101

102102

103+
def sanitize_url(url: str) -> str:
104+
"""Sanitize a URL.
105+
106+
Sanitize URLs, removing any user:password combinations and replacing them with
107+
asterisks. Returns the original URL if the string is a non-matching pattern.
108+
109+
:param url:
110+
str containing a URL to sanitize.
111+
112+
return:
113+
str either sanitized or as entered depending on pattern match.
114+
"""
115+
uri = rfc3986.urlparse(url)
116+
if uri.userinfo:
117+
return cast(str, uri.copy_with(userinfo="*" * 8).unsplit())
118+
return url
119+
120+
103121
def _validate_repository_url(repository_url: str) -> None:
104122
"""Validate the given url for allowed schemes and components."""
105123
# Allowed schemes are http and https, based on whether the repository
@@ -126,11 +144,7 @@ def get_repository_from_config(
126144
# Prefer CLI `repository_url` over `repository` or .pypirc
127145
if repository_url:
128146
_validate_repository_url(repository_url)
129-
return {
130-
"repository": repository_url,
131-
"username": None,
132-
"password": None,
133-
}
147+
return _config_from_repository_url(repository_url)
134148

135149
try:
136150
config = get_config(config_file)[repository]
@@ -154,6 +168,17 @@ def get_repository_from_config(
154168
}
155169

156170

171+
def _config_from_repository_url(url: str) -> RepositoryConfig:
172+
parsed = urlparse(url)
173+
config = {"repository": url, "username": None, "password": None}
174+
if parsed.username:
175+
config["username"] = parsed.username
176+
config["password"] = parsed.password
177+
config["repository"] = urlunparse((parsed.scheme, parsed.hostname) + parsed[2:])
178+
config["repository"] = normalize_repository_url(cast(str, config["repository"]))
179+
return config
180+
181+
157182
def normalize_repository_url(url: str) -> str:
158183
parsed = urlparse(url)
159184
if parsed.netloc in _HOSTNAMES:

0 commit comments

Comments
 (0)