From 78f7b8e7f7cdbb86d4df3a66aed4d850b6d49f62 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. --- launchable/utils/http_client.py | 17 ++++++++++++++- tests/utils/test_http_client.py | 38 ++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/launchable/utils/http_client.py b/launchable/utils/http_client.py index 0044ecb99..3cd865d25 100644 --- a/launchable/utils/http_client.py +++ b/launchable/utils/http_client.py @@ -91,7 +91,9 @@ def request( if additional_headers: headers = {**headers, **additional_headers} - Logger().audit(AUDIT_LOG_FORMAT.format("(DRY RUN) " if self.dry_run else "", method, url, headers, payload)) + dry_run_prefix = "(DRY RUN) " if self.dry_run else "" + 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={ @@ -199,5 +201,18 @@ def _build_data(payload: Optional[Union[BinaryIO, Dict]], 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 aadb886ce..395f886e1 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -2,7 +2,7 @@ import platform from unittest import TestCase, mock -from launchable.utils.http_client import _HttpClient +from launchable.utils.http_client import _HttpClient, _sanitize_headers from launchable.version import __version__ @@ -43,3 +43,39 @@ def test_header(self): "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')