From b16760b8615ef52c2497176519c32b6da7291094 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 14 Jul 2025 12:48:28 +0000 Subject: [PATCH] jsonpatch exception-mangling: more robust against secrets in dict keys --- electrum/json_db.py | 8 +++++++- electrum/util.py | 24 ++++++++++++++++++++++++ tests/test_jsondb.py | 10 ++++------ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index 34a74e259..ef7742b16 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -31,7 +31,7 @@ import jsonpatch import jsonpointer from . import util -from .util import WalletFileException, profiler +from .util import WalletFileException, profiler, sticky_property from .logging import Logger if TYPE_CHECKING: @@ -42,8 +42,14 @@ if TYPE_CHECKING: # We often log exceptions and offer to send them to the crash reporter, so they must not contain secrets. jsonpointer.JsonPointerException.__str__ = lambda self: """(JPE) 'redacted'""" jsonpointer.JsonPointerException.__repr__ = lambda self: """""" +setattr(jsonpointer.JsonPointerException, '__cause__', sticky_property(None)) +setattr(jsonpointer.JsonPointerException, '__context__', sticky_property(None)) +setattr(jsonpointer.JsonPointerException, '__suppress_context__', sticky_property(True)) jsonpatch.JsonPatchException.__str__ = lambda self: """(JPE) 'redacted'""" jsonpatch.JsonPatchException.__repr__ = lambda self: """""" +setattr(jsonpatch.JsonPatchException, '__cause__', sticky_property(None)) +setattr(jsonpatch.JsonPatchException, '__context__', sticky_property(None)) +setattr(jsonpatch.JsonPatchException, '__suppress_context__', sticky_property(True)) def modifier(func): diff --git a/electrum/util.py b/electrum/util.py index 7cb38faf8..d266a6a5c 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -2199,6 +2199,30 @@ class classproperty(property): return self.fget(owner_cls) +def sticky_property(val): + """Creates a 'property' whose value cannot be changed and that cannot be deleted. + Attempts to change the value are silently ignored. + + >>> class C: pass + ... + >>> setattr(C, 'x', sticky_property(3)) + >>> c = C() + >>> c.x + 3 + >>> c.x = 2 + >>> c.x + 3 + >>> del c.x + >>> c.x + 3 + """ + return property( + fget=lambda self: val, + fset=lambda *args, **kwargs: None, + fdel=lambda *args, **kwargs: None, + ) + + def get_running_loop() -> Optional[asyncio.AbstractEventLoop]: """Returns the asyncio event loop that is *running in this thread*, if any.""" try: diff --git a/tests/test_jsondb.py b/tests/test_jsondb.py index 83444d368..16129e114 100644 --- a/tests/test_jsondb.py +++ b/tests/test_jsondb.py @@ -39,14 +39,12 @@ class TestJsonpatch(ElectrumTestCase): def fail_if_leaking_secret(ctx) -> None: self.assertNotIn("secret", str(ctx.exception)) self.assertNotIn("secret", repr(ctx.exception)) + self.assertNotIn("secret", ctx._customctx_original_tb) + self.assertNotIn("dictlevel", str(ctx.exception)) + self.assertNotIn("dictlevel", repr(ctx.exception)) + self.assertNotIn("dictlevel", ctx._customctx_original_tb) self.assertIn("redacted", str(ctx.exception)) # injected by our monkeypatching self.assertIn("redacted", repr(ctx.exception)) # injected by our monkeypatching - self.assertNotIn("secret", ctx._customctx_original_tb) - # Note, crucially, the following assert would FAIL: - # That is, exceptions might "leak" the db *path* but not values stored at the innermost level. - # IOW, in case of dicts, secrets should be stored in values. Dict keys should never contain secrets, - # as dict keys can appear in tracebacks. - #self.assertNotIn("dictlevel", ctx._customctx_original_tb) # op "replace" with self.subTest(msg="replace_dict_inner_key_missing"): patches = [{"op": "replace", "path": "/dictlevelA1/dictlevelX2", "value": "nakamoto_secret"}]