fix: long variables pattern matching (#428)
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
# 7.8.3 - 2026-02-09
|
||||
|
||||
fix: do not pattern match long values in code variables
|
||||
|
||||
# 7.8.3 - 2026-02-06
|
||||
|
||||
fix: openAI input image sanitization
|
||||
|
||||
+39
-20
@@ -43,26 +43,29 @@ except ImportError:
|
||||
DEFAULT_MAX_VALUE_LENGTH = 1024
|
||||
|
||||
DEFAULT_CODE_VARIABLES_MASK_PATTERNS = [
|
||||
r"(?i).*password.*",
|
||||
r"(?i).*secret.*",
|
||||
r"(?i).*passwd.*",
|
||||
r"(?i).*pwd.*",
|
||||
r"(?i).*api_key.*",
|
||||
r"(?i).*apikey.*",
|
||||
r"(?i).*auth.*",
|
||||
r"(?i).*credentials.*",
|
||||
r"(?i).*privatekey.*",
|
||||
r"(?i).*private_key.*",
|
||||
r"(?i).*token.*",
|
||||
r"(?i).*aws_access_key_id.*",
|
||||
r"(?i).*_pass",
|
||||
r"(?i)sk_.*",
|
||||
r"(?i).*jwt.*",
|
||||
r"(?i)password",
|
||||
r"(?i)secret",
|
||||
r"(?i)passwd",
|
||||
r"(?i)pwd",
|
||||
r"(?i)api_key",
|
||||
r"(?i)apikey",
|
||||
r"(?i)auth",
|
||||
r"(?i)credentials",
|
||||
r"(?i)privatekey",
|
||||
r"(?i)private_key",
|
||||
r"(?i)token",
|
||||
r"(?i)aws_access_key_id",
|
||||
r"(?i)_pass",
|
||||
r"(?i)sk_",
|
||||
r"(?i)jwt",
|
||||
]
|
||||
|
||||
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]
|
||||
|
||||
CODE_VARIABLES_REDACTED_VALUE = "$$_posthog_redacted_based_on_masking_rules_$$"
|
||||
CODE_VARIABLES_TOO_LONG_VALUE = "$$_posthog_value_too_long_$$"
|
||||
|
||||
_MAX_VALUE_LENGTH_FOR_PATTERN_MATCH = 5_000
|
||||
|
||||
DEFAULT_TOTAL_VARIABLES_SIZE_LIMIT = 20 * 1024
|
||||
|
||||
@@ -945,23 +948,37 @@ def _pattern_matches(name, patterns):
|
||||
return False
|
||||
|
||||
|
||||
def _mask_sensitive_data(value, compiled_mask):
|
||||
def _mask_sensitive_data(value, compiled_mask, _seen=None):
|
||||
if not compiled_mask:
|
||||
return value
|
||||
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
obj_id = id(value)
|
||||
if obj_id in _seen:
|
||||
return "<circular ref>"
|
||||
_seen.add(obj_id)
|
||||
|
||||
if isinstance(value, dict):
|
||||
result = {}
|
||||
for k, v in value.items():
|
||||
key_str = str(k) if not isinstance(k, str) else k
|
||||
if _pattern_matches(key_str, compiled_mask):
|
||||
if len(key_str) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
|
||||
result[k] = CODE_VARIABLES_TOO_LONG_VALUE
|
||||
elif _pattern_matches(key_str, compiled_mask):
|
||||
result[k] = CODE_VARIABLES_REDACTED_VALUE
|
||||
else:
|
||||
result[k] = _mask_sensitive_data(v, compiled_mask)
|
||||
result[k] = _mask_sensitive_data(v, compiled_mask, _seen)
|
||||
return result
|
||||
elif isinstance(value, (list, tuple)):
|
||||
masked_items = [_mask_sensitive_data(item, compiled_mask) for item in value]
|
||||
masked_items = [
|
||||
_mask_sensitive_data(item, compiled_mask, _seen) for item in value
|
||||
]
|
||||
return type(value)(masked_items)
|
||||
elif isinstance(value, str):
|
||||
if len(value) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
|
||||
return CODE_VARIABLES_TOO_LONG_VALUE
|
||||
if _pattern_matches(value, compiled_mask):
|
||||
return CODE_VARIABLES_REDACTED_VALUE
|
||||
return value
|
||||
@@ -982,7 +999,9 @@ def _serialize_variable_value(value, limiter, max_length=1024, compiled_mask=Non
|
||||
limiter.add(result_size)
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
if compiled_mask and _pattern_matches(value, compiled_mask):
|
||||
if len(value) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
|
||||
result = CODE_VARIABLES_TOO_LONG_VALUE
|
||||
elif compiled_mask and _pattern_matches(value, compiled_mask):
|
||||
result = CODE_VARIABLES_REDACTED_VALUE
|
||||
else:
|
||||
result = value
|
||||
|
||||
@@ -450,3 +450,143 @@ def test_code_variables_repr_fallback(tmpdir):
|
||||
assert "<CustomReprClass: custom representation>" in output
|
||||
assert "<lambda>" in output
|
||||
assert "<function trigger_error at" in output
|
||||
|
||||
|
||||
def test_code_variables_too_long_string_value_replaced(tmpdir):
|
||||
app = tmpdir.join("app.py")
|
||||
app.write(
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
|
||||
posthog = Posthog(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
project_root=os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
|
||||
def trigger_error():
|
||||
short_value = "I am short"
|
||||
long_value = "x" * 20000
|
||||
long_blob = "password_" + "a" * 20000
|
||||
|
||||
1/0
|
||||
|
||||
trigger_error()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
||||
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
||||
|
||||
output = excinfo.value.output.decode("utf-8")
|
||||
|
||||
assert "ZeroDivisionError" in output
|
||||
assert "code_variables" in output
|
||||
|
||||
assert "'short_value': 'I am short'" in output
|
||||
|
||||
assert "$$_posthog_value_too_long_$$" in output
|
||||
|
||||
assert "'long_blob': '$$_posthog_value_too_long_$$'" in output
|
||||
|
||||
|
||||
def test_code_variables_too_long_string_in_nested_dict(tmpdir):
|
||||
app = tmpdir.join("app.py")
|
||||
app.write(
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
|
||||
posthog = Posthog(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
project_root=os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
|
||||
def trigger_error():
|
||||
my_data = {
|
||||
"short_key": "short_val",
|
||||
"long_key": "y" * 20000,
|
||||
"nested": {
|
||||
"deep_long": "z" * 20000,
|
||||
"deep_short": "ok",
|
||||
},
|
||||
}
|
||||
|
||||
1/0
|
||||
|
||||
trigger_error()
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
||||
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
|
||||
|
||||
output = excinfo.value.output.decode("utf-8")
|
||||
|
||||
assert "ZeroDivisionError" in output
|
||||
assert "code_variables" in output
|
||||
|
||||
assert "short_val" in output
|
||||
assert "ok" in output
|
||||
|
||||
assert "$$_posthog_value_too_long_$$" in output
|
||||
assert "y" * 1000 not in output
|
||||
assert "z" * 1000 not in output
|
||||
|
||||
|
||||
def test_mask_sensitive_data_too_long_dict_key():
|
||||
from posthog.exception_utils import (
|
||||
CODE_VARIABLES_TOO_LONG_VALUE,
|
||||
_compile_patterns,
|
||||
_mask_sensitive_data,
|
||||
)
|
||||
|
||||
compiled_mask = _compile_patterns([r"(?i)password"])
|
||||
|
||||
result = _mask_sensitive_data(
|
||||
{
|
||||
"short": "visible",
|
||||
"k" * 20000: "hidden_val",
|
||||
"password": "secret",
|
||||
},
|
||||
compiled_mask,
|
||||
)
|
||||
|
||||
assert result["short"] == "visible"
|
||||
# This then gets shortened by the JSON truncation at 1024 chars anyways so no worries
|
||||
assert result["k" * 20000] == CODE_VARIABLES_TOO_LONG_VALUE
|
||||
assert result["password"] == "$$_posthog_redacted_based_on_masking_rules_$$"
|
||||
|
||||
|
||||
def test_mask_sensitive_data_circular_ref():
|
||||
from posthog.exception_utils import _compile_patterns, _mask_sensitive_data
|
||||
|
||||
compiled_mask = _compile_patterns([r"(?i)password"])
|
||||
|
||||
# Circular dict
|
||||
circular_dict = {"key": "value"}
|
||||
circular_dict["self"] = circular_dict
|
||||
|
||||
result = _mask_sensitive_data(circular_dict, compiled_mask)
|
||||
assert result["key"] == "value"
|
||||
assert result["self"] == "<circular ref>"
|
||||
|
||||
# Circular list
|
||||
circular_list = ["item"]
|
||||
circular_list.append(circular_list)
|
||||
|
||||
result = _mask_sensitive_data(circular_list, compiled_mask)
|
||||
assert result[0] == "item"
|
||||
assert result[1] == "<circular ref>"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
VERSION = "7.8.3"
|
||||
VERSION = "7.8.4"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="") # noqa: T201
|
||||
|
||||
Reference in New Issue
Block a user