From bed3b02aa774c1ab582f98b8b34b81754c7a9cd1 Mon Sep 17 00:00:00 2001 From: Takayuki Watanabe Date: Thu, 16 Apr 2026 13:38:40 -0700 Subject: [PATCH] fix: Redact Authorization bearer token in debug logs Added _sanitize_headers() function to redact sensitive Authorization bearer tokens when logging HTTP requests in debug mode. The token value is replaced with [REDACTED] while preserving the "Bearer " prefix. We can use HTTP library for this but this needs library version upgrade. This is the most simplest way to work with small number of code. --- smart_tests/utils/http_client.py | 16 +++++++++++++- tests/utils/test_http_client.py | 38 +++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/smart_tests/utils/http_client.py b/smart_tests/utils/http_client.py index 61b37404e..ee6a9523d 100644 --- a/smart_tests/utils/http_client.py +++ b/smart_tests/utils/http_client.py @@ -89,7 +89,8 @@ def request( headers = {**headers, **additional_headers} dry_run_prefix = "(DRY RUN) " if self.dry_run else "" - Logger().audit(f"{dry_run_prefix}send request method:{method} path:{url} headers:{headers} args:{payload}") + sanitized_headers = _sanitize_headers(headers) + Logger().audit(f"{dry_run_prefix}send request method:{method} path:{url} headers:{sanitized_headers} args:{payload}") if self.dry_run and method.upper() not in ["HEAD", "GET"]: return DryRunResponse(status_code=200, payload={ @@ -165,5 +166,18 @@ def _build_data(payload: Union[BinaryIO, Dict] | None, compress: bool): return payload +def _sanitize_headers(headers: Dict) -> Dict: + """ + Returns a copy of headers with sensitive values redacted for logging. + """ + sanitized = headers.copy() + if 'Authorization' in sanitized: + auth_value = sanitized['Authorization'] + if auth_value.startswith('Bearer '): + # Redact the token but keep the "Bearer " prefix + sanitized['Authorization'] = 'Bearer [REDACTED]' + return sanitized + + def _join_paths(*components): return '/'.join([c.strip('/') for c in components]) diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index e1700b2ca..6333c5f82 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -3,7 +3,7 @@ from unittest import TestCase, mock from smart_tests.app import Application -from smart_tests.utils.http_client import _HttpClient +from smart_tests.utils.http_client import _HttpClient, _sanitize_headers from smart_tests.version import __version__ @@ -34,3 +34,39 @@ def test_header(self): "User-Agent": f"Launchable/{__version__} (Python {platform.python_version()}, " f"{platform.platform()}) TestRunner/dummy", }) + + def test_sanitize_headers_with_bearer_token(self): + headers = { + 'Authorization': 'Bearer v1:konboi/arm-testing:cfcxxxxxxxxxxxxxx', + 'Content-Type': 'application/json', + 'User-Agent': 'test-agent' + } + sanitized = _sanitize_headers(headers) + + # Original headers should not be modified + self.assertEqual(headers['Authorization'], 'Bearer v1:konboi/arm-testing:cfcxxxxxxxxxxxxxx') + + # Sanitized headers should have token redacted + self.assertEqual(sanitized['Authorization'], 'Bearer [REDACTED]') + self.assertEqual(sanitized['Content-Type'], 'application/json') + self.assertEqual(sanitized['User-Agent'], 'test-agent') + + def test_sanitize_headers_without_authorization(self): + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'test-agent' + } + sanitized = _sanitize_headers(headers) + + # All headers should remain unchanged + self.assertEqual(sanitized, headers) + + def test_sanitize_headers_with_non_bearer_authorization(self): + headers = { + 'Authorization': 'Basic dXNlcjpwYXNz', + 'Content-Type': 'application/json' + } + sanitized = _sanitize_headers(headers) + + # Non-Bearer authorization should remain unchanged + self.assertEqual(sanitized['Authorization'], 'Basic dXNlcjpwYXNz')