Avoid leaking extra-pattern matches in scrub reasons#2048
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a sensitive-data leak in Logfire’s scrubbing system where scrub markers could echo the matched secret substring when using ScrubbingOptions(extra_patterns=...), and adds regression coverage to prevent reintroducing the issue.
Changes:
- Compiles scrubbing regexes with per-pattern named groups to attribute a match to a specific configured pattern.
- Updates redaction “reason” generation to use the configured
extra_patternsregex string (instead of the matched substring) while preserving existing behavior for default patterns. - Adds a regression test for URL-credential scrubbing to ensure the scrub marker and
logfire.scrubbedmetadata do not contain the secret.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
logfire/_internal/scrubbing.py |
Changes scrubber regex compilation and redaction reason selection to avoid leaking matched secrets for extra_patterns. |
tests/test_secret_scrubbing.py |
Adds a regression test ensuring scrub reasons don’t echo URL credentials matched via extra_patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| matched_substring = match.pattern_match.group(0) | ||
| self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=matched_substring)) | ||
| return f'[Scrubbed due to {matched_substring!r}]' | ||
| reason = self._pattern_reason_by_group.get(match.pattern_match.lastgroup or '', matched_substring) or matched_substring | ||
| self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=reason)) | ||
| return f'[Scrubbed due to {reason!r}]' |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
📝 WalkthroughWalkthroughScrubbing now wraps each configured regex pattern in a named capture group and maps groups to default or extra-pattern reasons. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_secret_scrubbing.py (1)
340-358: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer the inline_snapshot pattern for span assertions.
The rest of this module (e.g.
test_scrubbing_config) asserts viaexporter.exported_spans_as_dict(...) == snapshot(...). This test hand-picks attributes instead. Keep thesecret not in ...guards (they document intent well), but add asnapshot()assertion so drift in the full span is caught.As per coding guidelines: "Tests that create spans should use TestExporter and inline_snapshot with the pattern... assert with exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot()".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_secret_scrubbing.py` around lines 340 - 358, The span assertion in test_extra_pattern_redaction_reason_does_not_echo_secret only checks selected attributes, so it can miss unrelated drift in the emitted span. Keep the existing secret-not-in guards, but update the assertion to use exporter.exported_spans_as_dict(parse_json_attributes=True) with inline snapshot() like the other tests in this module (for example test_scrubbing_config) so the full span shape is verified while preserving the redaction checks.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_secret_scrubbing.py`:
- Around line 340-358: The span assertion in
test_extra_pattern_redaction_reason_does_not_echo_secret only checks selected
attributes, so it can miss unrelated drift in the emitted span. Keep the
existing secret-not-in guards, but update the assertion to use
exporter.exported_spans_as_dict(parse_json_attributes=True) with inline
snapshot() like the other tests in this module (for example
test_scrubbing_config) so the full span shape is verified while preserving the
redaction checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b20ed9f-2f40-4786-aafa-beca62e01f17
📒 Files selected for processing (2)
logfire/_internal/scrubbing.pytests/test_secret_scrubbing.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
pydantic/pydantic(auto-detected)pydantic/pydantic-ai(auto-detected)
| assert secret not in config_url | ||
| assert secret not in scrubbed | ||
| assert config_url == "[Scrubbed due to '://[^:@/]+:[^@/]+@']" | ||
| assert scrubbed == IsJson([{'path': ['attributes', 'config_url'], 'matched_substring': '://[^:@/]+:[^@/]+@'}]) |
There was a problem hiding this comment.
'matched_substring': '://[^:@/]+:[^@/]+@' is inaccurate. That's clearly not the substring. We don't want to leak the actual substring, but having this leads to incorrect instructions. For example, this code:
import logfire
logfire.configure()
logfire.info('hi', foo='secret value')Produces this in the frontend:
|
Thanks, you're right that storing the configured regex as I updated the branch so matches from The regression test now uses a full span snapshot, and all 15 tests in |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@logfire/_internal/scrubbing.py`:
- Around line 356-366: Update the reason handling in the scrubbing method
containing _pattern_reason_by_group so extra-pattern matches never use
pattern_reason verbatim in the “[Scrubbed due to ...]” marker. Replace sensitive
or literal pattern-derived reasons with a fixed opaque label, while preserving
the existing ScrubbedNote behavior and default matched_substring reason for
other matches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 028c83b4-a4b1-459c-b641-32dfb20cf566
📒 Files selected for processing (2)
logfire/_internal/scrubbing.pytests/test_secret_scrubbing.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
pydantic/logfire(manual)pydantic/pydantic-ai(manual)pydantic/platform(auto-detected)pydantic/pydantic(auto-detected)
| reason = matched_substring | ||
| for group_name, pattern_reason in self._pattern_reason_by_group.items(): | ||
| if match.pattern_match.group(group_name) is not None: | ||
| if pattern_reason is None: | ||
| self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=matched_substring)) | ||
| else: | ||
| reason = pattern_reason | ||
| break | ||
| else: # pragma: no cover | ||
| self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=matched_substring)) | ||
| return f'[Scrubbed due to {reason!r}]' |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== scrubbing.py around cited lines ==\n'
sed -n '300,390p' logfire/_internal/scrubbing.py
printf '\n== search for extra_patterns and pattern_reason ==\n'
rg -n "extra_patterns|_pattern_reason_by_group|Scrubbed due to|ScrubbedNote|pattern_reason" logfire tests -SRepository: pydantic/logfire
Length of output: 12347
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '300,390p' logfire/_internal/scrubbing.py
rg -n "extra_patterns|_pattern_reason_by_group|Scrubbed due to|ScrubbedNote|pattern_reason" logfire tests -SRepository: pydantic/logfire
Length of output: 12256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '200,230p' logfire/_internal/scrubbing.pyRepository: pydantic/logfire
Length of output: 1470
Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File
Reachability: External
● Entry
tests/test_secret_scrubbing.py
│
▼
● Sink
logfire/_internal/scrubbing.py
Use opaque labels for extra-pattern scrub reasons logfire/_internal/scrubbing.py:210-216,356-366
extra_patterns are emitted verbatim in [Scrubbed due to ...], so a pattern built from a literal secret (for example re.escape(secret)) can leak that value into the marker. Use a non-sensitive label instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@logfire/_internal/scrubbing.py` around lines 356 - 366, Update the reason
handling in the scrubbing method containing _pattern_reason_by_group so
extra-pattern matches never use pattern_reason verbatim in the “[Scrubbed due to
...]” marker. Replace sensitive or literal pattern-derived reasons with a fixed
opaque label, while preserving the existing ScrubbedNote behavior and default
matched_substring reason for other matches.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="logfire/_internal/scrubbing.py">
<violation number="1" location="logfire/_internal/scrubbing.py:362">
P2: If a user constructs an `extra_patterns` entry from a literal secret (e.g., `re.escape(actual_secret)`), the secret will appear verbatim in the `[Scrubbed due to ...]` marker since `pattern_reason` is used directly. While this is somewhat of a user-misconfiguration scenario, this PR is specifically about preventing secret leakage in scrub reasons. Consider using an opaque label (e.g., `extra_pattern_<index>`) or documenting that patterns should not themselves contain sensitive literals.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| if pattern_reason is None: | ||
| self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=matched_substring)) | ||
| else: | ||
| reason = pattern_reason |
There was a problem hiding this comment.
P2: If a user constructs an extra_patterns entry from a literal secret (e.g., re.escape(actual_secret)), the secret will appear verbatim in the [Scrubbed due to ...] marker since pattern_reason is used directly. While this is somewhat of a user-misconfiguration scenario, this PR is specifically about preventing secret leakage in scrub reasons. Consider using an opaque label (e.g., extra_pattern_<index>) or documenting that patterns should not themselves contain sensitive literals.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At logfire/_internal/scrubbing.py, line 362:
<comment>If a user constructs an `extra_patterns` entry from a literal secret (e.g., `re.escape(actual_secret)`), the secret will appear verbatim in the `[Scrubbed due to ...]` marker since `pattern_reason` is used directly. While this is somewhat of a user-misconfiguration scenario, this PR is specifically about preventing secret leakage in scrub reasons. Consider using an opaque label (e.g., `extra_pattern_<index>`) or documenting that patterns should not themselves contain sensitive literals.</comment>
<file context>
@@ -353,11 +353,16 @@ def _redact(self, match: ScrubMatch) -> Any:
+ if pattern_reason is None:
+ self.scrubbed.append(ScrubbedNote(path=match.path, matched_substring=matched_substring))
+ else:
+ reason = pattern_reason
+ break
+ else: # pragma: no cover
</file context>
Summary
extra_patternsTesting
Fixes #1909