From f6c485101d16d79269dbf5e67cfe0b4b3a50509c Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Thu, 2 Apr 2026 17:19:30 +0200 Subject: [PATCH 01/13] Fix intersphinx leaking basic auth credentials in error and info messages --- sphinx/ext/intersphinx/_load.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index ab6a373fea0..4b954900159 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -401,7 +401,7 @@ def _fetch_inventory_url( except Exception as err: err.args = ( 'intersphinx inventory %r not fetchable due to %s: %s', - inv_location, + _get_safe_url(inv_location), err.__class__, str(err), ) @@ -409,7 +409,7 @@ def _fetch_inventory_url( if inv_location != new_inv_location: msg = __('intersphinx inventory has moved: %s -> %s') - LOGGER.info(msg, inv_location, new_inv_location) + LOGGER.info(msg, _get_safe_url(inv_location), _get_safe_url(new_inv_location)) if target_uri in { inv_location, From 712149430be9d37f77ff8f1f4fe1cec551cda1f6 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Mon, 6 Apr 2026 13:27:00 +0200 Subject: [PATCH 02/13] Add tests for credential redaction in error and redirect messages --- .../test_ext_intersphinx.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index 5fcfe4d9260..c87289dcc25 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -666,6 +666,53 @@ def test_getsafeurl_unauthed() -> None: assert actual == expected +@mock.patch('sphinx.ext.intersphinx._load.requests.get') +def test_fetch_inventory_url_error_hides_credentials(get_request): + """Credentials should not appear in error messages on fetch failure.""" + get_request.side_effect = Exception('connection refused') + with pytest.raises(Exception) as exc_info: + from sphinx.ext.intersphinx._load import _fetch_inventory_url + + _fetch_inventory_url( + target_uri='https://hostname/', + inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, + config=_InvConfig( + intersphinx_cache_limit=5, + intersphinx_timeout=None, + tls_verify=False, + tls_cacerts=None, + user_agent='', + ), + ) + # The password must not appear in the error args + error_text = str(exc_info.value.args) + assert 'secret' not in error_text + assert 'user@hostname' in error_text + + +@mock.patch('sphinx.ext.intersphinx._load.InventoryFile') +@mock.patch('sphinx.ext.intersphinx._load.requests.get') +@pytest.mark.sphinx('html', testroot='root') +def test_fetch_inventory_redirect_hides_credentials(get_request, InventoryFile, app): + """Credentials should not appear in redirect log messages.""" + mocked_get = get_request.return_value.__enter__.return_value + intersphinx_setup(app) + mocked_get.content = b'# Sphinx inventory version 2' + + mocked_get.url = 'https://user:secret@hostname/new/' + INVENTORY_FILENAME + + target_uri = 'https://hostname/' + raw_data, target_uri = _fetch_inventory_data( + target_uri=target_uri, + inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, + config=_InvConfig.from_config(app.config), + srcdir=app.srcdir, + cache_path=None, + ) + status_output = app.status.getvalue() + assert 'secret' not in status_output + + def test_inspect_main_noargs(capsys): """inspect_main interface, without arguments""" assert inspect_main([]) == 1 From f51a0b027bdc6c47a31ae6f0829711bcc914dfc7 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Mon, 6 Apr 2026 13:32:37 +0200 Subject: [PATCH 03/13] Fix ruff errors in credential redaction tests --- .../test_ext_intersphinx.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index c87289dcc25..a40f41ec397 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -669,10 +669,10 @@ def test_getsafeurl_unauthed() -> None: @mock.patch('sphinx.ext.intersphinx._load.requests.get') def test_fetch_inventory_url_error_hides_credentials(get_request): """Credentials should not appear in error messages on fetch failure.""" - get_request.side_effect = Exception('connection refused') - with pytest.raises(Exception) as exc_info: - from sphinx.ext.intersphinx._load import _fetch_inventory_url + from sphinx.ext.intersphinx._load import _fetch_inventory_url + get_request.side_effect = ConnectionError('connection refused') + with pytest.raises(ConnectionError, match='connection refused'): _fetch_inventory_url( target_uri='https://hostname/', inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, @@ -684,10 +684,23 @@ def test_fetch_inventory_url_error_hides_credentials(get_request): user_agent='', ), ) - # The password must not appear in the error args - error_text = str(exc_info.value.args) - assert 'secret' not in error_text - assert 'user@hostname' in error_text + # Also verify the rewritten error args don't contain the password + try: + _fetch_inventory_url( + target_uri='https://hostname/', + inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, + config=_InvConfig( + intersphinx_cache_limit=5, + intersphinx_timeout=None, + tls_verify=False, + tls_cacerts=None, + user_agent='', + ), + ) + except ConnectionError as exc: + error_text = str(exc.args) + assert 'secret' not in error_text + assert 'user@hostname' in error_text @mock.patch('sphinx.ext.intersphinx._load.InventoryFile') @@ -702,7 +715,7 @@ def test_fetch_inventory_redirect_hides_credentials(get_request, InventoryFile, mocked_get.url = 'https://user:secret@hostname/new/' + INVENTORY_FILENAME target_uri = 'https://hostname/' - raw_data, target_uri = _fetch_inventory_data( + _, target_uri = _fetch_inventory_data( target_uri=target_uri, inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, config=_InvConfig.from_config(app.config), From e467c92637c4879b59037ae4b06d9585616e6ab9 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Mon, 6 Apr 2026 22:15:46 +0200 Subject: [PATCH 04/13] Address review: rewrite tests to use inspect_main+capsys, fix credential leak in error string --- sphinx/ext/intersphinx/_load.py | 5 +- .../test_ext_intersphinx.py | 51 +++++++------------ 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 4b954900159..82766522a2c 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -399,11 +399,12 @@ def _fetch_inventory_url( raw_data = r.content new_inv_location = r.url except Exception as err: + safe_url = _get_safe_url(inv_location) err.args = ( 'intersphinx inventory %r not fetchable due to %s: %s', - _get_safe_url(inv_location), + safe_url, err.__class__, - str(err), + str(err).replace(inv_location, safe_url), ) raise diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index a40f41ec397..d8790486a5f 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -666,41 +666,23 @@ def test_getsafeurl_unauthed() -> None: assert actual == expected -@mock.patch('sphinx.ext.intersphinx._load.requests.get') -def test_fetch_inventory_url_error_hides_credentials(get_request): +def test_fetch_inventory_url_error_hides_credentials(capsys): """Credentials should not appear in error messages on fetch failure.""" - from sphinx.ext.intersphinx._load import _fetch_inventory_url - - get_request.side_effect = ConnectionError('connection refused') - with pytest.raises(ConnectionError, match='connection refused'): - _fetch_inventory_url( - target_uri='https://hostname/', - inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, - config=_InvConfig( - intersphinx_cache_limit=5, - intersphinx_timeout=None, - tls_verify=False, - tls_cacerts=None, - user_agent='', - ), - ) - # Also verify the rewritten error args don't contain the password - try: - _fetch_inventory_url( - target_uri='https://hostname/', - inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, - config=_InvConfig( - intersphinx_cache_limit=5, - intersphinx_timeout=None, - tls_verify=False, - tls_cacerts=None, - user_agent='', - ), - ) - except ConnectionError as exc: - error_text = str(exc.args) - assert 'secret' not in error_text - assert 'user@hostname' in error_text + + class ErrorHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_error(500, 'Internal Server Error') + + def log_message(*args, **kwargs): + pass + + with http_server(ErrorHandler) as server: + url = f'http://user:secret@localhost:{server.server_port}/{INVENTORY_FILENAME}' + inspect_main([url]) + + _, stderr = capsys.readouterr() + assert 'secret' not in stderr + assert 'user@localhost' in stderr @mock.patch('sphinx.ext.intersphinx._load.InventoryFile') @@ -724,6 +706,7 @@ def test_fetch_inventory_redirect_hides_credentials(get_request, InventoryFile, ) status_output = app.status.getvalue() assert 'secret' not in status_output + assert 'user@hostname' in status_output def test_inspect_main_noargs(capsys): From 3f96487a0a88c1cde8db91a0aae9d31753911d2b Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Tue, 7 Apr 2026 20:53:55 +0200 Subject: [PATCH 05/13] Replace mocks with http_server in redirect test for consistency --- .../test_ext_intersphinx.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index d8790486a5f..6fdb4bdd2f4 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -685,28 +685,41 @@ def log_message(*args, **kwargs): assert 'user@localhost' in stderr -@mock.patch('sphinx.ext.intersphinx._load.InventoryFile') -@mock.patch('sphinx.ext.intersphinx._load.requests.get') @pytest.mark.sphinx('html', testroot='root') -def test_fetch_inventory_redirect_hides_credentials(get_request, InventoryFile, app): +def test_fetch_inventory_redirect_hides_credentials(app): """Credentials should not appear in redirect log messages.""" - mocked_get = get_request.return_value.__enter__.return_value + + class RedirectHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if '/new/' not in self.path: + self.send_response(302) + new_url = f'http://localhost:{self.server.server_port}/new/{INVENTORY_FILENAME}' + self.send_header('Location', new_url) + self.end_headers() + else: + self.send_response(200, 'OK') + self.end_headers() + self.wfile.write(INVENTORY_V2) + + def log_message(*args, **kwargs): + pass + intersphinx_setup(app) - mocked_get.content = b'# Sphinx inventory version 2' - mocked_get.url = 'https://user:secret@hostname/new/' + INVENTORY_FILENAME + with http_server(RedirectHandler) as server: + port = server.server_port + inv_location = f'http://user:secret@localhost:{port}/{INVENTORY_FILENAME}' + _fetch_inventory_data( + target_uri=f'http://localhost:{port}/', + inv_location=inv_location, + config=_InvConfig.from_config(app.config), + srcdir=app.srcdir, + cache_path=None, + ) - target_uri = 'https://hostname/' - _, target_uri = _fetch_inventory_data( - target_uri=target_uri, - inv_location='https://user:secret@hostname/' + INVENTORY_FILENAME, - config=_InvConfig.from_config(app.config), - srcdir=app.srcdir, - cache_path=None, - ) status_output = app.status.getvalue() assert 'secret' not in status_output - assert 'user@hostname' in status_output + assert 'user@localhost' in status_output def test_inspect_main_noargs(capsys): From d154e84369a57725a611e8debbe294eac2d1e58f Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 8 Apr 2026 11:38:46 +0200 Subject: [PATCH 06/13] Assert credentials absent from both stdout and stderr in error test --- tests/test_ext_intersphinx/test_ext_intersphinx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index 6fdb4bdd2f4..d57755a273d 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -680,7 +680,8 @@ def log_message(*args, **kwargs): url = f'http://user:secret@localhost:{server.server_port}/{INVENTORY_FILENAME}' inspect_main([url]) - _, stderr = capsys.readouterr() + stdout, stderr = capsys.readouterr() + assert 'secret' not in stdout assert 'secret' not in stderr assert 'user@localhost' in stderr From c38ee69c1ecfef030ab1e07c6bb8f9430132a84e Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 8 Apr 2026 15:44:30 +0200 Subject: [PATCH 07/13] Rewrite redirect test to use http_server + inspect_main + caplog for consistency --- .../test_ext_intersphinx.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index d57755a273d..9aef0d63596 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -686,8 +686,7 @@ def log_message(*args, **kwargs): assert 'user@localhost' in stderr -@pytest.mark.sphinx('html', testroot='root') -def test_fetch_inventory_redirect_hides_credentials(app): +def test_fetch_inventory_redirect_hides_credentials(capsys, caplog): """Credentials should not appear in redirect log messages.""" class RedirectHandler(http.server.BaseHTTPRequestHandler): @@ -705,22 +704,15 @@ def do_GET(self): def log_message(*args, **kwargs): pass - intersphinx_setup(app) - with http_server(RedirectHandler) as server: port = server.server_port - inv_location = f'http://user:secret@localhost:{port}/{INVENTORY_FILENAME}' - _fetch_inventory_data( - target_uri=f'http://localhost:{port}/', - inv_location=inv_location, - config=_InvConfig.from_config(app.config), - srcdir=app.srcdir, - cache_path=None, - ) + url = f'http://user:secret@localhost:{port}/{INVENTORY_FILENAME}' + inspect_main([url]) - status_output = app.status.getvalue() - assert 'secret' not in status_output - assert 'user@localhost' in status_output + stdout, stderr = capsys.readouterr() + full_output = stdout + stderr + '\n'.join(caplog.messages) + assert 'secret' not in full_output + assert 'user@localhost' in full_output def test_inspect_main_noargs(capsys): From 5ab2973059b2c2ff0ae7254df1dec3e1c3e83a13 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 8 Apr 2026 16:35:19 +0200 Subject: [PATCH 08/13] Check each output source separately to avoid false positives from concatenation --- tests/test_ext_intersphinx/test_ext_intersphinx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index 9aef0d63596..928a2328a5c 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -710,9 +710,10 @@ def log_message(*args, **kwargs): inspect_main([url]) stdout, stderr = capsys.readouterr() - full_output = stdout + stderr + '\n'.join(caplog.messages) - assert 'secret' not in full_output - assert 'user@localhost' in full_output + assert 'secret' not in stdout + assert 'secret' not in stderr + assert not any('secret' in message for message in caplog.messages) + assert any('user@localhost' in message for message in caplog.messages) def test_inspect_main_noargs(capsys): From 1ed61548b4e31f35e779fd6f3888bde25427b31f Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 8 Apr 2026 17:22:03 +0200 Subject: [PATCH 09/13] Add caplog to error test and inline port variable in redirect test --- tests/test_ext_intersphinx/test_ext_intersphinx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index 928a2328a5c..6653d5661fc 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -666,7 +666,7 @@ def test_getsafeurl_unauthed() -> None: assert actual == expected -def test_fetch_inventory_url_error_hides_credentials(capsys): +def test_fetch_inventory_url_error_hides_credentials(capsys, caplog): """Credentials should not appear in error messages on fetch failure.""" class ErrorHandler(http.server.BaseHTTPRequestHandler): @@ -683,6 +683,7 @@ def log_message(*args, **kwargs): stdout, stderr = capsys.readouterr() assert 'secret' not in stdout assert 'secret' not in stderr + assert not any('secret' in message for message in caplog.messages) assert 'user@localhost' in stderr @@ -705,8 +706,7 @@ def log_message(*args, **kwargs): pass with http_server(RedirectHandler) as server: - port = server.server_port - url = f'http://user:secret@localhost:{port}/{INVENTORY_FILENAME}' + url = f'http://user:secret@localhost:{server.server_port}/{INVENTORY_FILENAME}' inspect_main([url]) stdout, stderr = capsys.readouterr() From b8bc837f9bf93fd533dd392fefa6a87b5d847133 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Mon, 13 Apr 2026 12:47:58 +0200 Subject: [PATCH 10/13] Add changelog entry for intersphinx credential leak fix --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6f7279e8011..1ade267dc11 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,9 @@ Features added Bugs fixed ---------- +* #14365: intersphinx: Fix leaking basic auth credentials in error and + redirect log messages. + Patch by Joshua Swanson * #14189: autodoc: Fix duplicate ``:no-index-entry:`` for modules. Patch by Adam Turner * #13713: Fix compatibility with MyST-Parser. From c14aa0048833d14c97a2c43f7818ae08b7a1b8cd Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 19 Aug 2026 17:47:16 +0200 Subject: [PATCH 11/13] Fix issue number in changelog entry --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1ade267dc11..9f4b2b74544 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,7 @@ Features added Bugs fixed ---------- -* #14365: intersphinx: Fix leaking basic auth credentials in error and +* #14342: intersphinx: Fix leaking basic auth credentials in error and redirect log messages. Patch by Joshua Swanson * #14189: autodoc: Fix duplicate ``:no-index-entry:`` for modules. From d95fd565c525b65ad84faa5ba6660f1234fffbf2 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Fri, 21 Aug 2026 00:53:53 +0200 Subject: [PATCH 12/13] Redact normalized request and response URLs in intersphinx fetch errors --- sphinx/ext/intersphinx/_load.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 82766522a2c..9fb08892069 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -400,11 +400,20 @@ def _fetch_inventory_url( new_inv_location = r.url except Exception as err: safe_url = _get_safe_url(inv_location) + # The URLs retained by the exception may be normalised forms of + # *inv_location* (e.g. percent-decoded), so redact them separately. + err_msg = str(err).replace(inv_location, safe_url) + for unsafe_url in ( + getattr(getattr(err, 'request', None), 'url', None), + getattr(getattr(err, 'response', None), 'url', None), + ): + if unsafe_url: + err_msg = err_msg.replace(unsafe_url, _get_safe_url(unsafe_url)) err.args = ( 'intersphinx inventory %r not fetchable due to %s: %s', safe_url, err.__class__, - str(err).replace(inv_location, safe_url), + err_msg, ) raise From a9f1ce34721b85245e33246e4c873bb8a3a40ce9 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Fri, 21 Aug 2026 00:53:53 +0200 Subject: [PATCH 13/13] Strengthen credential redaction tests for redirects and URL normalization --- .../test_ext_intersphinx.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/test_ext_intersphinx/test_ext_intersphinx.py b/tests/test_ext_intersphinx/test_ext_intersphinx.py index 6653d5661fc..fb83d303ae7 100644 --- a/tests/test_ext_intersphinx/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx/test_ext_intersphinx.py @@ -3,6 +3,7 @@ from __future__ import annotations import http.server +import logging import time from typing import TYPE_CHECKING from unittest import mock @@ -666,8 +667,11 @@ def test_getsafeurl_unauthed() -> None: assert actual == expected -def test_fetch_inventory_url_error_hides_credentials(capsys, caplog): +def test_fetch_inventory_url_error_hides_credentials(capsys, caplog, monkeypatch): """Credentials should not appear in error messages on fetch failure.""" + # A previously created Sphinx app disables propagation on the 'sphinx' + # logger; caplog needs it to capture the messages. + monkeypatch.setattr(logging.getLogger('sphinx'), 'propagate', True) class ErrorHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): @@ -677,24 +681,37 @@ def log_message(*args, **kwargs): pass with http_server(ErrorHandler) as server: - url = f'http://user:secret@localhost:{server.server_port}/{INVENTORY_FILENAME}' + # %65 is an 'e'; requests normalises the URL it retains on the + # exception, so the password can leak in a form ('secret') that + # differs from the one passed in ('s%65cret'). + url = ( + f'http://user:s%65cret@localhost:{server.server_port}/{INVENTORY_FILENAME}' + ) inspect_main([url]) stdout, stderr = capsys.readouterr() - assert 'secret' not in stdout - assert 'secret' not in stderr - assert not any('secret' in message for message in caplog.messages) + for leak in ('secret', 's%65cret'): + assert leak not in stdout + assert leak not in stderr + assert not any(leak in message for message in caplog.messages) assert 'user@localhost' in stderr -def test_fetch_inventory_redirect_hides_credentials(capsys, caplog): +def test_fetch_inventory_redirect_hides_credentials(capsys, caplog, monkeypatch): """Credentials should not appear in redirect log messages.""" + # A previously created Sphinx app disables propagation on the 'sphinx' + # logger; caplog needs it to capture the messages. + monkeypatch.setattr(logging.getLogger('sphinx'), 'propagate', True) class RedirectHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if '/new/' not in self.path: self.send_response(302) - new_url = f'http://localhost:{self.server.server_port}/new/{INVENTORY_FILENAME}' + assert isinstance(self.server, http.server.HTTPServer) + new_url = ( + 'http://redirect-user:redirect-secret@localhost:' + f'{self.server.server_port}/new/{INVENTORY_FILENAME}' + ) self.send_header('Location', new_url) self.end_headers() else: @@ -714,6 +731,7 @@ def log_message(*args, **kwargs): assert 'secret' not in stderr assert not any('secret' in message for message in caplog.messages) assert any('user@localhost' in message for message in caplog.messages) + assert any('redirect-user@localhost' in message for message in caplog.messages) def test_inspect_main_noargs(capsys):