Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-otters-mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pypi/posthog': minor
---

Mask sensitive data held inside objects and in URL/DSN credentials when capturing exception code variables. Custom objects are now traversed so fields like `password` are redacted by attribute name instead of leaking via `repr()`, and credentials embedded in connection strings are scrubbed. Adds the `code_variables_mask_url_credentials` option (default `True`).
14 changes: 14 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
from posthog.contexts import (
set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
)
from posthog.contexts import (
set_code_variables_mask_url_credentials_context as inner_set_code_variables_mask_url_credentials_context,
)
from posthog.contexts import (
set_context_device_id as inner_set_context_device_id,
)
Expand All @@ -39,6 +42,7 @@
from posthog.exception_utils import (
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
)
from posthog.feature_flag_evaluations import (
FeatureFlagEvaluations as FeatureFlagEvaluations,
Expand Down Expand Up @@ -226,6 +230,14 @@ def set_code_variables_ignore_patterns_context(ignore_patterns: list):
return inner_set_code_variables_ignore_patterns_context(ignore_patterns)


def set_code_variables_mask_url_credentials_context(enabled: bool):
"""
Whether to scrub credentials embedded in URLs/DSNs (e.g. user:pass@host) from
captured code variables for the current context.
"""
return inner_set_code_variables_mask_url_credentials_context(enabled)


def tag(name: str, value: Any):
"""
Add a tag to the current context.
Expand Down Expand Up @@ -346,6 +358,7 @@ def get_tags() -> Dict[str, Any]:
capture_exception_code_variables = False
code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
code_variables_mask_url_credentials = DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
in_app_modules = None # type: Optional[list[str]]
enable_exception_autocapture_rate_limiting = False # type: bool
exception_autocapture_bucket_size = ExceptionCapture.DEFAULT_BUCKET_SIZE # type: int
Expand Down Expand Up @@ -1124,6 +1137,7 @@ def setup() -> Client:
capture_exception_code_variables=capture_exception_code_variables,
code_variables_mask_patterns=code_variables_mask_patterns,
code_variables_ignore_patterns=code_variables_ignore_patterns,
code_variables_mask_url_credentials=code_variables_mask_url_credentials,
in_app_modules=in_app_modules,
enable_exception_autocapture_rate_limiting=enable_exception_autocapture_rate_limiting,
exception_autocapture_bucket_size=exception_autocapture_bucket_size,
Expand Down
20 changes: 20 additions & 0 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
get_capture_exception_code_variables_context,
get_code_variables_ignore_patterns_context,
get_code_variables_mask_patterns_context,
get_code_variables_mask_url_credentials_context,
get_context_device_id,
get_context_distinct_id,
get_context_session_id,
Expand All @@ -37,6 +38,7 @@
from posthog.exception_utils import (
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
exc_info_from_error,
exception_is_already_captured,
exceptions_from_error_tuple,
Expand Down Expand Up @@ -239,6 +241,7 @@ def __init__(
capture_exception_code_variables=False,
code_variables_mask_patterns=None,
code_variables_ignore_patterns=None,
code_variables_mask_url_credentials=None,
in_app_modules: list[str] | None = None,
enable_exception_autocapture_rate_limiting=False,
exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE,
Expand Down Expand Up @@ -304,6 +307,9 @@ def __init__(
capturing code variables.
code_variables_ignore_patterns: Variable-name patterns to omit when
capturing code variables.
code_variables_mask_url_credentials: Scrub credentials embedded in
URLs/DSNs (e.g. ``user:pass@host``) from captured code variables,
regardless of the surrounding variable name. Defaults to True.
in_app_modules: Module/package prefixes treated as in-app frames in
captured exceptions.
enable_exception_autocapture_rate_limiting: Rate limit
Expand Down Expand Up @@ -396,6 +402,11 @@ def __init__(
if code_variables_ignore_patterns is not None
else DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
)
self.code_variables_mask_url_credentials = (
code_variables_mask_url_credentials
if code_variables_mask_url_credentials is not None
else DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
)
self.in_app_modules = in_app_modules

if project_root is None:
Expand Down Expand Up @@ -1327,6 +1338,9 @@ def capture_exception(
context_enabled = get_capture_exception_code_variables_context()
context_mask = get_code_variables_mask_patterns_context()
context_ignore = get_code_variables_ignore_patterns_context()
context_mask_url_credentials = (
get_code_variables_mask_url_credentials_context()
)

enabled = (
context_enabled
Expand All @@ -1343,13 +1357,19 @@ def capture_exception(
if context_ignore is not None
else self.code_variables_ignore_patterns
)
mask_url_credentials = (
context_mask_url_credentials
if context_mask_url_credentials is not None
else self.code_variables_mask_url_credentials
)

if enabled:
try_attach_code_variables_to_frames(
all_exceptions_with_trace_and_in_app,
exc_info,
mask_patterns=mask_patterns,
ignore_patterns=ignore_patterns,
mask_url_credentials=mask_url_credentials,
)

if self.log_captured_exceptions:
Expand Down
28 changes: 28 additions & 0 deletions posthog/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(
self.capture_exception_code_variables: Optional[bool] = None
self.code_variables_mask_patterns: Optional[list] = None
self.code_variables_ignore_patterns: Optional[list] = None
self.code_variables_mask_url_credentials: Optional[bool] = None

def set_session_id(self, session_id: str):
self.session_id = session_id
Expand All @@ -48,6 +49,9 @@ def set_code_variables_mask_patterns(self, mask_patterns: list):
def set_code_variables_ignore_patterns(self, ignore_patterns: list):
self.code_variables_ignore_patterns = ignore_patterns

def set_code_variables_mask_url_credentials(self, enabled: bool):
self.code_variables_mask_url_credentials = enabled

def get_parent(self):
return self.parent

Expand Down Expand Up @@ -102,6 +106,13 @@ def get_code_variables_ignore_patterns(self) -> Optional[list]:
return self.parent.get_code_variables_ignore_patterns()
return None

def get_code_variables_mask_url_credentials(self) -> Optional[bool]:
if self.code_variables_mask_url_credentials is not None:
return self.code_variables_mask_url_credentials
if self.parent is not None and not self.fresh:
return self.parent.get_code_variables_mask_url_credentials()
return None


_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar(
"posthog_context_stack", default=None
Expand Down Expand Up @@ -369,6 +380,16 @@ def set_code_variables_ignore_patterns_context(ignore_patterns: list) -> None:
current_context.set_code_variables_ignore_patterns(ignore_patterns)


def set_code_variables_mask_url_credentials_context(enabled: bool) -> None:
"""
Whether to scrub credentials embedded in URLs/DSNs (e.g. user:pass@host) from
captured code variables for the current context.
"""
current_context = _get_current_context()
if current_context:
current_context.set_code_variables_mask_url_credentials(enabled)


def get_capture_exception_code_variables_context() -> Optional[bool]:
current_context = _get_current_context()
if current_context:
Expand All @@ -390,6 +411,13 @@ def get_code_variables_ignore_patterns_context() -> Optional[list]:
return None


def get_code_variables_mask_url_credentials_context() -> Optional[bool]:
current_context = _get_current_context()
if current_context:
return current_context.get_code_variables_mask_url_credentials()
return None


F = TypeVar("F", bound=Callable[..., Any])


Expand Down
Loading
Loading