From a2c64a8c915d4c8a48b89309567f40b80a5cc361 Mon Sep 17 00:00:00 2001 From: mic1on Date: Sat, 11 Jul 2026 20:40:05 +0800 Subject: [PATCH] fix: avoid over-redacting ordinary urls Keep ordinary single-segment URLs visible while still redacting sensitive-looking custom notification paths.\n\nRefs #44 --- src/use_notify/redaction.py | 25 +++++++++++++++++++++++-- tests/test_notification.py | 13 +++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/use_notify/redaction.py b/src/use_notify/redaction.py index dfa959c..e39a389 100644 --- a/src/use_notify/redaction.py +++ b/src/use_notify/redaction.py @@ -9,7 +9,12 @@ re.compile(r"(?i)(/open-apis/bot/v2/hook/)[^/?\s)]+"), re.compile(r"(?i)(ntfy\.sh/)[^/?\s)]+"), ) -_SINGLE_SEGMENT_URL_SECRET_RE = re.compile(r"(?i)(https?://[^/?#\s)]+/)[^/?#\s)]+(?=([?#\s)]|$))") +_SINGLE_SEGMENT_URL_RE = re.compile( + r"(?i)(?Phttps?://[^/?#\s)]+/)(?P[^/?#\s)]+)(?=(?:[?#\s)]|$))" +) +_SENSITIVE_SEGMENT_RE = re.compile( + r"(?i)(?:^|[-_.])(?:token|secret|pushkey|topic|api[-_.]?key|access[-_.]?key|key)(?:$|[-_.]|\d)" +) def redact_text(value: str) -> str: @@ -17,7 +22,7 @@ def redact_text(value: str) -> str: redacted = _QUERY_SECRET_RE.sub(rf"\1{SECRET_REPLACEMENT}", value) for pattern in _PATH_SECRET_PATTERNS: redacted = pattern.sub(rf"\1{SECRET_REPLACEMENT}", redacted) - redacted = _SINGLE_SEGMENT_URL_SECRET_RE.sub(rf"\1{SECRET_REPLACEMENT}", redacted) + redacted = _SINGLE_SEGMENT_URL_RE.sub(_redact_sensitive_single_segment_url, redacted) return redacted @@ -28,3 +33,19 @@ def redact_exception_message(error: Exception) -> Exception: error.args = tuple(redact_text(arg) if isinstance(arg, str) else arg for arg in error.args) return error + + +def _redact_sensitive_single_segment_url(match: re.Match) -> str: + segment = match.group("segment") + if _looks_sensitive_path_segment(segment): + return f"{match.group('prefix')}{SECRET_REPLACEMENT}" + return match.group(0) + + +def _looks_sensitive_path_segment(segment: str) -> bool: + if _SENSITIVE_SEGMENT_RE.search(segment): + return True + + has_alpha = any(character.isalpha() for character in segment) + has_digit = any(character.isdigit() for character in segment) + return len(segment) >= 16 and has_alpha and has_digit diff --git a/tests/test_notification.py b/tests/test_notification.py index ee0d1fb..6378301 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -183,14 +183,17 @@ def test_redacts_custom_base_url_path_secrets(): message = ( "failed https://bark.example.com/bark-secret-token " "and https://ntfy.example.com/my-secret-topic" + " and https://notify.example.com/abc123def456ghi7" ) redacted = redact_text(message) assert "bark-secret-token" not in redacted assert "my-secret-topic" not in redacted + assert "abc123def456ghi7" not in redacted assert "https://bark.example.com/" in redacted assert "https://ntfy.example.com/" in redacted + assert "https://notify.example.com/" in redacted def test_redaction_keeps_multi_segment_urls_visible(): @@ -199,6 +202,16 @@ def test_redaction_keeps_multi_segment_urls_visible(): assert redact_text(message) == message +def test_redaction_keeps_ordinary_single_segment_urls_visible(): + message = ( + "docs https://example.com/docs " + "and status https://status.example.com/health " + "and keyboard https://example.com/keyboard" + ) + + assert redact_text(message) == message + + def test_publisher_copies_initial_channel_collection(): initial_channels = [RecordingChannel()] publisher = Publisher(initial_channels)