Skip to content

Commit 8752022

Browse files
authored
fix(security): prevent credential exposure in logs (#167)
- Truncate access key to first/last 4 chars in DEBUG logs (P2-001) - Add _sanitize_headers() to redact Authorization, x-amz-security-token, and x-amz-date values in debug logs (P2-002) - Add comprehensive unit tests for header sanitization Security: Prevents credential leakage in log files, monitoring systems, and log aggregation where DEBUG level may be enabled for troubleshooting.
1 parent d48aa77 commit 8752022

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

mcp_proxy_for_aws/sigv4_helper.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@
2727

2828
logger = logging.getLogger(__name__)
2929

30+
# Headers that should be redacted when logging to prevent credential exposure
31+
SENSITIVE_HEADERS = frozenset({'authorization', 'x-amz-security-token', 'x-amz-date'})
32+
33+
34+
def _sanitize_headers(headers: Dict[str, str]) -> Dict[str, str]:
35+
"""Redact sensitive header values for safe logging.
36+
37+
Args:
38+
headers: Dictionary of HTTP headers
39+
40+
Returns:
41+
Dictionary with sensitive values replaced by '[REDACTED]'
42+
"""
43+
return {k: '[REDACTED]' if k.lower() in SENSITIVE_HEADERS else v for k, v in headers.items()}
44+
3045

3146
class SigV4HTTPXAuth(httpx.Auth):
3247
"""HTTPX Auth class that signs requests with AWS SigV4."""
@@ -236,7 +251,11 @@ async def _sign_request_hook(
236251

237252
# Get AWS credentials from the session
238253
credentials = session.get_credentials()
239-
logger.info('Signing request with credentials for access key: %s', credentials.access_key)
254+
logger.debug(
255+
'Signing request with credentials for access key: %s...%s',
256+
credentials.access_key[:4],
257+
credentials.access_key[-4:],
258+
)
240259

241260
# Create SigV4 auth and use its signing logic
242261
auth = SigV4HTTPXAuth(credentials, service, region)
@@ -245,7 +264,7 @@ async def _sign_request_hook(
245264
auth_flow = auth.auth_flow(request)
246265
next(auth_flow) # Execute the generator to perform signing
247266

248-
logger.debug('Request headers after signing: %s', request.headers)
267+
logger.debug('Request headers after signing: %s', _sanitize_headers(dict(request.headers)))
249268

250269

251270
async def _inject_metadata_hook(metadata: Dict[str, Any], request: httpx.Request) -> None:

tests/unit/test_sigv4_helper.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import httpx
1818
import pytest
1919
from mcp_proxy_for_aws.sigv4_helper import (
20+
SENSITIVE_HEADERS,
2021
SigV4HTTPXAuth,
22+
_sanitize_headers,
2123
create_aws_session,
2224
create_sigv4_client,
2325
)
@@ -254,3 +256,77 @@ def test_create_sigv4_client_with_prompt_context(self, mock_client_class, mock_c
254256
assert len(call_args[1]['event_hooks']['response']) == 1
255257

256258
assert result == mock_client
259+
260+
261+
class TestSanitizeHeaders:
262+
"""Test cases for the _sanitize_headers function."""
263+
264+
def test_sanitize_headers_redacts_authorization(self):
265+
"""Test that Authorization header is redacted."""
266+
headers = {
267+
'Authorization': 'AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/...',
268+
'Content-Type': 'application/json',
269+
}
270+
result = _sanitize_headers(headers)
271+
272+
assert result['Authorization'] == '[REDACTED]'
273+
assert result['Content-Type'] == 'application/json'
274+
275+
def test_sanitize_headers_redacts_security_token(self):
276+
"""Test that x-amz-security-token header is redacted."""
277+
headers = {
278+
'x-amz-security-token': 'FwoGZXIvYXdzEBYaDK...',
279+
'Host': 'example.com',
280+
}
281+
result = _sanitize_headers(headers)
282+
283+
assert result['x-amz-security-token'] == '[REDACTED]'
284+
assert result['Host'] == 'example.com'
285+
286+
def test_sanitize_headers_redacts_amz_date(self):
287+
"""Test that x-amz-date header is redacted."""
288+
headers = {
289+
'X-Amz-Date': '20260206T120000Z',
290+
'Accept': 'application/json',
291+
}
292+
result = _sanitize_headers(headers)
293+
294+
assert result['X-Amz-Date'] == '[REDACTED]'
295+
assert result['Accept'] == 'application/json'
296+
297+
def test_sanitize_headers_case_insensitive(self):
298+
"""Test that header matching is case-insensitive."""
299+
headers = {
300+
'AUTHORIZATION': 'secret',
301+
'X-AMZ-SECURITY-TOKEN': 'secret',
302+
'x-amz-date': 'secret',
303+
}
304+
result = _sanitize_headers(headers)
305+
306+
assert result['AUTHORIZATION'] == '[REDACTED]'
307+
assert result['X-AMZ-SECURITY-TOKEN'] == '[REDACTED]'
308+
assert result['x-amz-date'] == '[REDACTED]'
309+
310+
def test_sanitize_headers_preserves_non_sensitive(self):
311+
"""Test that non-sensitive headers are preserved."""
312+
headers = {
313+
'Content-Type': 'application/json',
314+
'Content-Length': '123',
315+
'Host': 'example.amazonaws.com',
316+
'User-Agent': 'test-client/1.0',
317+
}
318+
result = _sanitize_headers(headers)
319+
320+
assert result == headers
321+
322+
def test_sanitize_headers_empty_dict(self):
323+
"""Test handling of empty headers dictionary."""
324+
result = _sanitize_headers({})
325+
assert result == {}
326+
327+
def test_sensitive_headers_constant_is_frozen(self):
328+
"""Test that SENSITIVE_HEADERS is immutable."""
329+
assert isinstance(SENSITIVE_HEADERS, frozenset)
330+
assert 'authorization' in SENSITIVE_HEADERS
331+
assert 'x-amz-security-token' in SENSITIVE_HEADERS
332+
assert 'x-amz-date' in SENSITIVE_HEADERS

0 commit comments

Comments
 (0)