1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414import abc
15- import base64
16- import copy
17- import pickle
1815import typing
1916import unittest
2017from pyglove .core .symbolic import unknown_symbols
@@ -351,8 +348,7 @@ def test_json_conversion_for_methods(self):
351348 )
352349
353350 def test_json_conversion_for_opaque_objects (self ):
354- with json_conversion .allow_opaque_pickle ():
355- self .assert_conversion_equal (X (1 ))
351+ self .assert_conversion_equal (X (1 ))
356352
357353 class LocalX :
358354 pass
@@ -363,11 +359,9 @@ class LocalX:
363359
364360 json_dict = json_conversion .to_json (X (1 ))
365361 json_dict ['value' ] = 'abc'
366- with json_conversion .allow_opaque_pickle ():
367- with self .assertRaisesRegex (
368- ValueError , 'Cannot decode opaque object with pickle.'
369- ):
370- json_conversion .from_json (json_dict )
362+ with self .assertRaisesRegex (
363+ ValueError , 'Cannot decode opaque object with pickle.' ):
364+ json_conversion .from_json (json_dict )
371365
372366 def test_json_conversion_convert_unknown (self ):
373367 self .assertEqual (
@@ -493,8 +487,7 @@ def to_json(self, **kwargs):
493487 }
494488 }
495489 )
496- with json_conversion .allow_opaque_pickle ():
497- y_prime = json_conversion .from_json (y_json )
490+ y_prime = json_conversion .from_json (y_json )
498491 self .assertIs (y_prime ['t' ], y_prime ['v' ][1 ])
499492 self .assertIs (y_prime ['u' ], y_prime ['v' ][0 ])
500493
@@ -534,136 +527,5 @@ def test_json_conversion_with_sharing_convert_unknown(self):
534527 ]
535528 )
536529
537- def test_opaque_object_not_in_registry (self ):
538- """_OpaqueObject must not be reachable via type registry."""
539- # _OpaqueObject should NOT be auto-registered, preventing attackers from
540- # crafting a JSON payload that triggers pickle.loads on untrusted data.
541- opaque_typename = json_conversion ._type_name (json_conversion ._OpaqueObject )
542- self .assertFalse (
543- json_conversion .JSONConvertible .is_registered (opaque_typename ),
544- f'_OpaqueObject is registered under { opaque_typename !r} . '
545- 'This allows RCE via pickle deserialization from untrusted JSON.' ,
546- )
547-
548- def test_opaque_object_rce_blocked (self ):
549- """Malicious JSON targeting _OpaqueObject must be rejected."""
550-
551- # Simulate an attacker's payload: a pickle bomb inside _OpaqueObject JSON.
552- class _Canary :
553- triggered = False
554-
555- def __reduce__ (self ):
556- # If this runs, the attacker wins.
557- _Canary .triggered = True
558- return (int , (0 ,))
559-
560- malicious_payload = {
561- '_type' : 'pyglove.core.utils.json_conversion._OpaqueObject' ,
562- 'value' : base64 .encodebytes (pickle .dumps (_Canary ())).decode ('utf-8' ),
563- }
564- # pickle.dumps calls __reduce__ during serialization, so reset the flag
565- # to only detect execution during the deserialization (attack) path.
566- _Canary .triggered = False
567- # Deserialization must reject the unregistered type, NOT unpickle it.
568- with self .assertRaises (TypeError ):
569- json_conversion .from_json (malicious_payload )
570- self .assertFalse (
571- _Canary .triggered ,
572- 'Pickle payload was executed — RCE vulnerability is still present!' ,
573- )
574-
575- def test_opaque_from_json_gate_without_context_manager (self ):
576- """Direct _OpaqueObject.from_json must be gated."""
577- # Even calling from_json directly (bypassing the registry) must fail.
578- x = X (1 )
579- opaque = json_conversion ._OpaqueObject (x )
580- json_value = opaque .to_json ()
581- with self .assertRaisesRegex (TypeError , 'disabled by default' ):
582- json_conversion ._OpaqueObject .from_json (json_value )
583-
584- def test_opaque_from_json_works_inside_context_manager (self ):
585- """from_json works when explicitly opted-in."""
586- x = X (1 )
587- opaque = json_conversion ._OpaqueObject (x )
588- json_value = opaque .to_json ()
589- with json_conversion .allow_opaque_pickle ():
590- result = json_conversion ._OpaqueObject .from_json (json_value )
591- self .assertEqual (result , x )
592-
593- def test_allow_opaque_pickle_restores_on_exception (self ):
594- """Flag must be restored even if the body raises."""
595- self .assertFalse (json_conversion ._opaque_pickle_enabled )
596- try :
597- with json_conversion .allow_opaque_pickle ():
598- self .assertTrue (json_conversion ._opaque_pickle_enabled )
599- raise RuntimeError ('simulated crash' )
600- except RuntimeError :
601- pass
602- # Flag must be restored to False after exception.
603- self .assertFalse (json_conversion ._opaque_pickle_enabled )
604-
605- def test_allow_opaque_pickle_nested (self ):
606- """Nested context managers must restore correctly."""
607- self .assertFalse (json_conversion ._opaque_pickle_enabled )
608- with json_conversion .allow_opaque_pickle ():
609- self .assertTrue (json_conversion ._opaque_pickle_enabled )
610- with json_conversion .allow_opaque_pickle ():
611- self .assertTrue (json_conversion ._opaque_pickle_enabled )
612- # After inner exits, flag should still be True (outer is active).
613- self .assertTrue (json_conversion ._opaque_pickle_enabled )
614- # After outer exits, flag must be False.
615- self .assertFalse (json_conversion ._opaque_pickle_enabled )
616-
617- def test_opaque_rce_blocked_via_auto_import (self ):
618- """auto_import must NOT bypass the pickle gate."""
619- x = X (1 )
620- json_dict = json_conversion .to_json (x )
621- # from_json mutates the dict in-place (pops _type), so use copies.
622- # Even with auto_import=True (default), deserialization must fail.
623- with self .assertRaises (TypeError ):
624- json_conversion .from_json (copy .deepcopy (json_dict ))
625- # With opt-in, it works.
626- with json_conversion .allow_opaque_pickle ():
627- result = json_conversion .from_json (copy .deepcopy (json_dict ))
628- self .assertEqual (result , x )
629-
630- def test_opaque_to_json_always_works (self ):
631- """Serialization must never be gated."""
632- # to_json must work even when the flag is off (it's only from_json
633- # that is dangerous).
634- self .assertFalse (json_conversion ._opaque_pickle_enabled )
635- x = X (1 )
636- json_dict = json_conversion .to_json (x )
637- self .assertIn ('_type' , json_dict )
638- opaque_typename = json_conversion ._type_name (json_conversion ._OpaqueObject )
639- self .assertEqual (json_dict ['_type' ], opaque_typename )
640- self .assertIn ('value' , json_dict )
641-
642- def test_opaque_rce_blocked_with_nested_payload (self ):
643- """Nested malicious _OpaqueObject in a list must be blocked."""
644- nested_payload = [
645- 1 ,
646- 'safe' ,
647- {
648- '_type' : 'pyglove.core.utils.json_conversion._OpaqueObject' ,
649- 'value' : base64 .encodebytes (pickle .dumps (42 )).decode ('utf-8' ),
650- },
651- ]
652- with self .assertRaises (TypeError ):
653- json_conversion .from_json (nested_payload )
654-
655- def test_opaque_rce_blocked_with_dict_payload (self ):
656- """_OpaqueObject inside a dict value must be blocked."""
657- dict_payload = {
658- 'safe_key' : 'safe_value' ,
659- 'malicious' : {
660- '_type' : 'pyglove.core.utils.json_conversion._OpaqueObject' ,
661- 'value' : base64 .encodebytes (pickle .dumps (42 )).decode ('utf-8' ),
662- },
663- }
664- with self .assertRaises (TypeError ):
665- json_conversion .from_json (dict_payload )
666-
667-
668530if __name__ == '__main__' :
669531 unittest .main ()
0 commit comments