[English | 中文]
In Python, all classes have a largely useless __subclasses__() method. This implementation not only incurs some memory overhead but also makes the code less secure. Users can access any built-in functions and classes through object.__subclasses__(), which undermines the complete security of exec and eval functions.
The no-subclasses library is designed to remove the __subclasses__() method from all classes, thereby reducing Python's memory overhead and achieving almost complete security for exec and eval functions, preventing all __subclasses__ attacks within exec and eval.
By default, Python's __subclasses__() can include almost any class. After enabling the no_subclasses library, __subclasses__() will always return an empty list, even if new classes are defined.
>>> import no_subclasses
>>> len(object.__subclasses__()) # Without no_subclasses library
313
>>> object.__subclasses__()[:5]
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>]
>>>
>>> no_subclasses.init() # Enable no_subclasses library
>>> object.__subclasses__()
[]
>>> int.__subclasses__()
[]
>>> type.__subclasses__(type)
[]Additionally, the library provides secure exec and eval functions, which cannot call any built-in functions or classes, nor can they be exploited by calling any class's __subclasses__() method.
>>> from no_subclasses import init,safe_eval
>>>
>>> safe_scope = {"__builtins__":{}} # Cannot call any built-in functions
>>> attack_expr = "(1).__class__.__base__.__subclasses__()"
>>> eval(attack_expr,safe_scope) # eval before enabling no_subclasses
[<class 'type'>, <class 'async_generator'>, <class 'int'>,
<class 'bytearray_iterator'>, <class 'bytearray'>,
<class 'bytes_iterator'>, <class 'bytes'>,
<class 'PyCapsule'>,<class 'classmethod'>,...] # Contains many built-in functions, insecure
>>>
>>> init()
>>> safe_eval(attack_expr) # or eval(attack_expr,safe_scope)
[]-
hack_class(cls): Clears the__subclasses__()list of a class. -
hack_all_classes(start = object, ignored=()): Starting from a root class (default isobject), clears the__subclasses__list of all subclasses.ignoredis a list or tuple of classes to be ignored, default is empty. -
init_build_class_hook(): Modifies the built-in__build_class__function so that executing aclassstatement does not modify the__subclasses__()list again, eliminating the need to re-callhack_class(). -
init_type_hook(): Modifies the built-intype()andtype.__new__()so that callingtype()ortype.__new__()does not modify the__subclasses__()list again, eliminating the need to re-callhack_class(). (Requirespydetourlibrary) -
init(): Initializes the entireno_subclasseslibrary, callinghack_all_classes,init_build_class_hook, andinit_type_hooktogether. (Recommended)
The hack_class method is implemented based on the lower-level pyobject library's get_type_subclasses and set_type_subclasses methods. The hack_all_classes method uses BFS to deeply search all subclasses and then calls hack_class on each class.