Skip to content

Avoid leaking extra-pattern matches in scrub reasons#2048

Open
Whning0513 wants to merge 4 commits into
pydantic:mainfrom
Whning0513:fix-extra-pattern-scrub-reason-1909
Open

Avoid leaking extra-pattern matches in scrub reasons#2048
Whning0513 wants to merge 4 commits into
pydantic:mainfrom
Whning0513:fix-extra-pattern-scrub-reason-1909

Conversation

@Whning0513

@Whning0513 Whning0513 commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • compile scrub patterns with per-pattern groups so we can identify which configured pattern matched
  • keep existing default scrub reasons, but use the configured extra pattern string instead of the matched secret substring
  • add a regression test covering URL credential scrubbing via extra_patterns

Testing

  • python -m pytest -q tests/test_secret_scrubbing.py -k extra_pattern_redaction_reason_does_not_echo_secret
  • python -m pytest -q tests/test_secret_scrubbing.py
  • python -m pytest -q tests/test_print.py -k instrument_print

Fixes #1909

Review in cubic

Copilot AI review requested due to automatic review settings July 1, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_patterns regex 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.scrubbed metadata 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.

Comment on lines 355 to +358
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}]'

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@hramezani
hramezani requested a review from alexmojaki July 2, 2026 07:36
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Scrubbing now wraps each configured regex pattern in a named capture group and maps groups to default or extra-pattern reasons. SpanScrubber copies this mapping, and _redact uses the originating extra pattern instead of the matched substring in redaction markers. A test verifies that PostgreSQL connection URL credentials are absent from exported span data and scrub reasons.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: preventing extra-pattern scrub reasons from exposing matched secrets.
Description check ✅ Passed The description is directly about the implemented scrubbing fix and regression test.
Linked Issues check ✅ Passed The changes address #1909 by avoiding matched-secret leakage in extra-pattern scrub reasons while preserving default behavior.
Out of Scope Changes check ✅ Passed The modified code and test stay focused on the scrubbing bug fix with no obvious unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_secret_scrubbing.py (1)

340-358: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the inline_snapshot pattern for span assertions.

The rest of this module (e.g. test_scrubbing_config) asserts via exporter.exported_spans_as_dict(...) == snapshot(...). This test hand-picks attributes instead. Keep the secret not in ... guards (they document intent well), but add a snapshot() 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

📥 Commits

Reviewing files that changed from the base of the PR and between ef5c776 and 5560f63.

📒 Files selected for processing (2)
  • logfire/_internal/scrubbing.py
  • tests/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)

Comment thread tests/test_secret_scrubbing.py Outdated
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': '://[^:@/]+:[^@/]+@'}])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'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:

Image

@Whning0513

Copy link
Copy Markdown
Author

Thanks, you're right that storing the configured regex as matched_substring gives the frontend incorrect input.

I updated the branch so matches from extra_patterns still use the configured pattern in the safe replacement marker, but do not add a logfire.scrubbed entry. Default-pattern matches keep their existing metadata. I also replaced the lastgroup lookup with an explicit check of the generated outer groups, so custom regexes containing capture groups work correctly.

The regression test now uses a full span snapshot, and all 15 tests in test_secret_scrubbing.py pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5560f63 and d2d208c.

📒 Files selected for processing (2)
  • logfire/_internal/scrubbing.py
  • tests/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)

Comment on lines +356 to +366
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}]'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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 -S

Repository: 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 -S

Repository: pydantic/logfire

Length of output: 12256


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '200,230p' logfire/_internal/scrubbing.py

Repository: 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ScrubbingOptions extra_patterns: scrub-message leaks the matched credential substring

3 participants