Skip to content

fix(telemetry): filter sensitive kwargs in error path of track_usage decorator#826

Open
sebastiondev wants to merge 2 commits into
nottelabs:mainfrom
sebastiondev:fix/cwe200-telemetry-sensitive-b8f9-clean
Open

fix(telemetry): filter sensitive kwargs in error path of track_usage decorator#826
sebastiondev wants to merge 2 commits into
nottelabs:mainfrom
sebastiondev:fix/cwe200-telemetry-sensitive-b8f9-clean

Conversation

@sebastiondev
Copy link
Copy Markdown

@sebastiondev sebastiondev commented May 14, 2026

Vulnerability: Sensitive credentials leaked to external telemetry on errors (CWE-200)

Affected file: packages/notte-core/src/notte_core/common/telemetry.py
Affected function: track_usage decorator (line ~237)
Severity: High — credentials sent to third-party services

Summary

The track_usage decorator filters sensitive kwargs (password, email, username, mfa_secret) before sending telemetry events on the success path, but the error/exception path bypasses this filtering entirely. When a decorated function raises an exception, the raw unfiltered kwargs dict — including plaintext passwords and secrets — is sent to external telemetry services (PostHog, Scarf).

This directly affects the vault credential endpoints in packages/notte-sdk/src/notte_sdk/endpoints/vaults.py, where functions like add_or_update_credentials accept sensitive fields (password, email, username, mfa_secret) as kwargs and are decorated with @track_usage.

Data flow

  1. User calls vault_client.add_or_update_credentials(vault_id, password="s3cret", email="user@example.com")
  2. track_usage decorator wraps the call
  3. If the underlying API call raises (network error, auth failure, server error, etc.), the except block fires
  4. Before this fix: capture_event() is called with raw kwargs containing password and email
  5. capture_event() forwards to PostHog and Scarf — external third-party services
  6. Plaintext credentials are now stored in third-party telemetry backends

PoC

from notte_core.common.telemetry import track_usage

@track_usage("test.leak")
def demo_leak(vault_id: str, password: str = "", email: str = ""):
    raise ConnectionError("simulated API failure")

# On the unfixed code, this sends password="s3cret" and email="me@example.com"
# to PostHog/Scarf telemetry endpoints
try:
    demo_leak("vault-123", password="s3cret", email="me@example.com")
except ConnectionError:
    pass

To confirm leakage, you can temporarily patch capture_event to print its properties argument — you'll see the raw password in the error-path payload but not in the success-path payload.

Fix

The fix hoists the filtered_kwargs computation above the try block so both the success and error paths use the same filtered dict. This is a 2-line change: move the filtering line up by 1 line, and replace kwargs with filtered_kwargs in the except block.

Before:

try:
    result = func(*args, **kwargs)
    filtered_kwargs = {k: v for k, v in kwargs.items() if k not in exclude_kwargs}
    capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
    return result
except Exception as e:
    capture_event(event_name, properties={"input": {"args": args, "kwargs": kwargs}, "error": str(e)})
    raise e

After:

filtered_kwargs = {k: v for k, v in kwargs.items() if k not in exclude_kwargs}
try:
    result = func(*args, **kwargs)
    capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
    return result
except Exception as e:
    capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}, "error": str(e)})
    raise e

Testing

  • Verified the decorator still correctly filters password, email, username, and mfa_secret on the success path
  • Verified the error path now uses filtered_kwargs instead of raw kwargs
  • Confirmed that the exclude_kwargs set matches the sensitive fields used by vault credential endpoints

Adversarial review

Before submitting, we considered whether this exposure is mitigated by other controls. It is not — telemetry is enabled by default (NOTTE_DISABLE_TELEMETRY must be explicitly set), the capture_event function sends data to external PostHog and Scarf endpoints over HTTPS, and there is no secondary filtering or redaction layer between the decorator and the HTTP calls. Any exception in a vault credential operation (network timeout, 401, server 500) triggers the leak. The filtering logic already existed for the success path, confirming the project's intent to exclude these fields from telemetry — the error path was simply missed.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.


Note

Fixes a security bug in the track_usage decorator where sensitive kwargs (password, email, username, mfa_secret) were correctly filtered on the success path but sent raw to external telemetry (PostHog, Scarf) on the exception path. The fix hoists filtered_kwargs computation above the try block so both paths use the same filtered dict. The second commit applies ruff format to address the previous review comment.

Written by Mendral for commit 88d9732.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 14, 2026

PR author is not in the allowed authors list.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

This pull request modifies the track_usage decorator in the telemetry module to consistently handle sensitive keyword arguments. The error handler path now uses filtered_kwargs (which excludes email, username, password, and mfa_secret) instead of the unfiltered kwargs when recording telemetry events. This ensures sensitive data is not exposed in error event properties, matching the behavior already applied to successful execution paths.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the security fix: filtering sensitive kwargs in the error path of the track_usage decorator. It is concise, specific, and clearly summarizes the main change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

mendral-app[bot]

This comment was marked as outdated.

@2027-evals
Copy link
Copy Markdown

2027-evals Bot commented May 14, 2026

⚠️ Couldn't find a preview deployment for commit 88d9732 after 10 minutes.

2027 auto-runs evals against preview deployments of your docs. To enable this, install one of:

  • Mintlify — if you use Mintlify docs
  • Vercel — for Next.js / static sites
  • Netlify — for most static docs

Once a preview is deployed, open a new PR and we'll run the eval automatically.


Evaluating agent experience using 2027.dev · View dashboard

@sebastiondev
Copy link
Copy Markdown
Author

Thanks for the review! You're right — the formatting was the only thing blocking CI. I've run ruff format on the file and pushed the result (88d9732). The change just wraps the long capture_event call in the except block across multiple lines.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 `@packages/notte-core/src/notte_core/common/telemetry.py`:
- Around line 241-249: The current try/except captures success telemetry inside
the try and error telemetry inside except; refactor to use try/except/else in
the wrapper around func so the success-path capture_event(event_name,
properties={...}) runs in an else block (executed only if no exception), keep
the error capture in except using the same properties plus "error": str(e), and
re-raise the exception with plain raise (not raise e) to preserve the traceback;
update references to func, event_name, capture_event, and filtered_kwargs
accordingly.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: b9bde868-e12b-4fa2-b83e-e6b6760ee8c0

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb616e and 88d9732.

📒 Files selected for processing (1)
  • packages/notte-core/src/notte_core/common/telemetry.py

Comment on lines 241 to 249
try:
result = func(*args, **kwargs)
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in exclude_kwargs}
capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
return result
except Exception as e:
capture_event(event_name, properties={"input": {"args": args, "kwargs": kwargs}, "error": str(e)})
capture_event(
event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}, "error": str(e)}
)
raise e
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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider using try/except/else for clearer control flow.

The success-path telemetry on line 243 could be moved to an else block to make it explicit that it only runs when no exception occurs. This follows Python best practices and addresses the Ruff TRY300 hint.

♻️ Proposed refactor using try/except/else
 try:
     result = func(*args, **kwargs)
-    capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
-    return result
 except Exception as e:
     capture_event(
         event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}, "error": str(e)}
     )
     raise e
+else:
+    capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
+    return result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
result = func(*args, **kwargs)
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in exclude_kwargs}
capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
return result
except Exception as e:
capture_event(event_name, properties={"input": {"args": args, "kwargs": kwargs}, "error": str(e)})
capture_event(
event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}, "error": str(e)}
)
raise e
try:
result = func(*args, **kwargs)
except Exception as e:
capture_event(
event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}, "error": str(e)}
)
raise e
else:
capture_event(event_name, properties={"input": {"args": args, "kwargs": filtered_kwargs}})
return result
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 244-244: Consider moving this statement to an else block

(TRY300)


[warning] 249-249: Use raise without specifying exception name

Remove exception name

(TRY201)

🤖 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 `@packages/notte-core/src/notte_core/common/telemetry.py` around lines 241 -
249, The current try/except captures success telemetry inside the try and error
telemetry inside except; refactor to use try/except/else in the wrapper around
func so the success-path capture_event(event_name, properties={...}) runs in an
else block (executed only if no exception), keep the error capture in except
using the same properties plus "error": str(e), and re-raise the exception with
plain raise (not raise e) to preserve the traceback; update references to func,
event_name, capture_event, and filtered_kwargs accordingly.

Copy link
Copy Markdown

@mendral-app mendral-app Bot left a comment

Choose a reason for hiding this comment

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

LGTM

Previous formatting comment addressed. The fix is correct and complete. CI failures are pre-existing on the base branch and unrelated to this change.

Tag @mendral-app with feedback or questions. View session

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.

1 participant