jsonpatch exception-mangling: more robust against secrets in dict keys
This commit is contained in:
@@ -31,7 +31,7 @@ import jsonpatch
|
|||||||
import jsonpointer
|
import jsonpointer
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
from .util import WalletFileException, profiler
|
from .util import WalletFileException, profiler, sticky_property
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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.
|
# 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.__str__ = lambda self: """(JPE) 'redacted'"""
|
||||||
jsonpointer.JsonPointerException.__repr__ = lambda self: """<JsonPointerException 'redacted'>"""
|
jsonpointer.JsonPointerException.__repr__ = lambda self: """<JsonPointerException 'redacted'>"""
|
||||||
|
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.__str__ = lambda self: """(JPE) 'redacted'"""
|
||||||
jsonpatch.JsonPatchException.__repr__ = lambda self: """<JsonPatchException 'redacted'>"""
|
jsonpatch.JsonPatchException.__repr__ = lambda self: """<JsonPatchException 'redacted'>"""
|
||||||
|
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):
|
def modifier(func):
|
||||||
|
|||||||
@@ -2199,6 +2199,30 @@ class classproperty(property):
|
|||||||
return self.fget(owner_cls)
|
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]:
|
def get_running_loop() -> Optional[asyncio.AbstractEventLoop]:
|
||||||
"""Returns the asyncio event loop that is *running in this thread*, if any."""
|
"""Returns the asyncio event loop that is *running in this thread*, if any."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -39,14 +39,12 @@ class TestJsonpatch(ElectrumTestCase):
|
|||||||
def fail_if_leaking_secret(ctx) -> None:
|
def fail_if_leaking_secret(ctx) -> None:
|
||||||
self.assertNotIn("secret", str(ctx.exception))
|
self.assertNotIn("secret", str(ctx.exception))
|
||||||
self.assertNotIn("secret", repr(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", str(ctx.exception)) # injected by our monkeypatching
|
||||||
self.assertIn("redacted", repr(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"
|
# op "replace"
|
||||||
with self.subTest(msg="replace_dict_inner_key_missing"):
|
with self.subTest(msg="replace_dict_inner_key_missing"):
|
||||||
patches = [{"op": "replace", "path": "/dictlevelA1/dictlevelX2", "value": "nakamoto_secret"}]
|
patches = [{"op": "replace", "path": "/dictlevelA1/dictlevelX2", "value": "nakamoto_secret"}]
|
||||||
|
|||||||
Reference in New Issue
Block a user