From c54ec98942376db5413167c39097a772bb34c172 Mon Sep 17 00:00:00 2001 From: Gareth Price Date: Mon, 6 Apr 2026 20:22:04 +0100 Subject: [PATCH 1/2] fix(exceptions): redact sensitive patterns from cause messages in PhloError output Security fix: Exception cause messages could leak sensitive data like passwords, tokens, connection strings, etc. Changes: - Added _SENSITIVE_PATTERNS regex list for common sensitive patterns - Added _redact_sensitive() helper to redact matched patterns - Updated PhloError._format_message() to redact sensitive data in cause - Added test to verify redaction works correctly Preserves diagnostic value for non-sensitive errors while protecting against credential leaks in error output. --- src/phlo/exceptions.py | 20 +++++++++++++++++++- tests/test_exceptions.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/phlo/exceptions.py b/src/phlo/exceptions.py index 7eb9c69ca..16549119e 100644 --- a/src/phlo/exceptions.py +++ b/src/phlo/exceptions.py @@ -4,8 +4,24 @@ Structured error classes with error codes, contextual messages, and suggestions. """ +import re from enum import Enum +_SENSITIVE_PATTERNS = ( + re.compile(r"(password|passwd|token|secret|api_key|apikey|credential)[=:]\S+", re.IGNORECASE), + re.compile(r"(authorization|bearer)\s+\S+", re.IGNORECASE), + re.compile(r"(private_key|signing_key|encryption_key)\s+", re.IGNORECASE), + re.compile(r"connection\s+string[=:]\S+", re.IGNORECASE), +) + + +def _redact_sensitive(s: str) -> str: + """Redact sensitive patterns from a string for safe output.""" + result = s + for pattern in _SENSITIVE_PATTERNS: + result = pattern.sub(lambda m: f"{m.group(0).split('=')[0].split()[0]}=", result) + return result + class PhloErrorCode(Enum): """Error codes for Phlo exceptions.""" @@ -98,7 +114,9 @@ def _format_message(self, message: str) -> str: if self.cause: lines.append("") - lines.append(f"Caused by: {type(self.cause).__name__}: {str(self.cause)}") + lines.append( + f"Caused by: {type(self.cause).__name__}: {_redact_sensitive(str(self.cause))}" + ) lines.append("") lines.append(f"Documentation: {self.doc_url}") diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5ef48411f..1d8d7d965 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -85,3 +85,15 @@ def test_format_field_list_handles_empty_field_list() -> None: formatted = format_field_list([]) assert formatted == "" + + +def test_phlo_error_redacts_sensitive_data_in_cause() -> None: + """Sensitive patterns in cause messages are redacted.""" + error = PhloError( + message="Connection failed", + code=PhloErrorCode.INFRASTRUCTURE_ERROR, + cause=ValueError("connection string: password=secret123"), + ) + message = str(error) + assert "Caused by: ValueError: connection string: password=" in message + assert "secret123" not in message From 60c2094e217ae0466379f12982d428a99dc4ffa3 Mon Sep 17 00:00:00 2001 From: Gareth Price Date: Mon, 6 Apr 2026 20:25:30 +0100 Subject: [PATCH 2/2] fix: harden exception secret redaction --- src/phlo/exceptions.py | 28 +++++++++++++++++++--------- tests/test_exceptions.py | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/phlo/exceptions.py b/src/phlo/exceptions.py index 16549119e..be5b4f26c 100644 --- a/src/phlo/exceptions.py +++ b/src/phlo/exceptions.py @@ -7,20 +7,30 @@ import re from enum import Enum -_SENSITIVE_PATTERNS = ( - re.compile(r"(password|passwd|token|secret|api_key|apikey|credential)[=:]\S+", re.IGNORECASE), - re.compile(r"(authorization|bearer)\s+\S+", re.IGNORECASE), - re.compile(r"(private_key|signing_key|encryption_key)\s+", re.IGNORECASE), - re.compile(r"connection\s+string[=:]\S+", re.IGNORECASE), +_KEY_VALUE_SENSITIVE_PATTERN = re.compile( + r"\b(password|passwd|token|secret|api_key|apikey|credential)\b\s*[:=]\s*[^\s,;]+", + re.IGNORECASE, +) +_AUTHORIZATION_SENSITIVE_PATTERN = re.compile(r"\b(authorization|bearer)\b\s+\S+", re.IGNORECASE) +_CONNECTION_STRING_SENSITIVE_PATTERN = re.compile( + r"\b(connection\s+string)\b\s*[:=]\s*.+?(?=(?:[,;]\s+\w+\s*[:=])|\n|$)", + re.IGNORECASE, +) +_KEY_MATERIAL_SENSITIVE_PATTERN = re.compile( + r"\b(private_key|signing_key|encryption_key)\b(?:\s*[:=]\s*|\s+).+?(?=(?:[,;]\s+\w+\s*[:=])|\n|$)", + re.IGNORECASE, ) def _redact_sensitive(s: str) -> str: """Redact sensitive patterns from a string for safe output.""" - result = s - for pattern in _SENSITIVE_PATTERNS: - result = pattern.sub(lambda m: f"{m.group(0).split('=')[0].split()[0]}=", result) - return result + result = _KEY_MATERIAL_SENSITIVE_PATTERN.sub(r"\1=", s) + result = _CONNECTION_STRING_SENSITIVE_PATTERN.sub(r"\1=", result) + result = _KEY_VALUE_SENSITIVE_PATTERN.sub( + lambda m: f"{m.group(1)}=", + result, + ) + return _AUTHORIZATION_SENSITIVE_PATTERN.sub(r"\1 ", result) class PhloErrorCode(Enum): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1d8d7d965..e95778699 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -5,6 +5,7 @@ from phlo.exceptions import ( PhloError, PhloErrorCode, + _redact_sensitive, format_field_list, suggest_similar_field_names, ) @@ -95,5 +96,22 @@ def test_phlo_error_redacts_sensitive_data_in_cause() -> None: cause=ValueError("connection string: password=secret123"), ) message = str(error) - assert "Caused by: ValueError: connection string: password=" in message + assert "Caused by: ValueError: connection string=" in message assert "secret123" not in message + + +def test_redact_sensitive_handles_colon_delimited_secret_values() -> None: + """Common `key: value` secret formats are redacted.""" + assert _redact_sensitive("password: secret123") == "password=" + assert _redact_sensitive("token: abc123") == "token=" + assert _redact_sensitive("connection string: Server=db;Password=hunter2") == ( + "connection string=" + ) + + +def test_redact_sensitive_removes_private_key_material() -> None: + """Key labels and their body are redacted together.""" + redacted = _redact_sensitive("private_key PEM_BLOCK_BODY_XYZ") + + assert redacted == "private_key=" + assert "PEM_BLOCK_BODY_XYZ" not in redacted