2017-08-18 5 views
1

Je rencontre des problèmes avec les classes de sérialisation YAML qui ont des références de type en tant que membres. J'utilise le chargeur sécurisé de ruamel.yaml.YAML - Attributs de sérialisation qui sont des types

J'ai exécuté tout ce qui suit à partir d'une invite REPL (pour obtenir plusieurs erreurs).

Initialisation:

import sys 
from ruamel.yaml import YAML, yaml_object 

Y = YAML(typ="safe",pure=True) 

# ============== 

@yaml_object(Y) 
class A(object): 
    """Object I want to serialize""" 
    yaml_tag = "!Aclass" 
    def __init__(self, type): 
     self.type = type 
    def f(self): 
     return self.type() 
    pass 

class T1(object): 
    """This will be referenced.""" 
    pass 

@yaml_object(Y) 
class T2(object): 
    """Another referenced object""" 
    pass 

class T3(object): 
    """Yet another try""" 
    pass 
Y.register_class(T3.__class__) 
code

qui provoque un échec:

Y.dump(A(T1), sys.stdout) 
Y.dump(A(T2), sys.stdout) 
Y.dump(A(T3), sys.stdout) 
Y.dump(A(int), sys.stdout) 

Ce sorties (seulement dernières lignes de retraçage):

ruamel.yaml.representer.RepresenterError: cannot represent an object: <attribute '__dict__' of 'T1' objects> 
ruamel.yaml.representer.RepresenterError: cannot represent an object: <attribute '__dict__' of 'T2' objects> 
ruamel.yaml.representer.RepresenterError: cannot represent an object: <attribute '__dict__' of 'T3' objects> 
ruamel.yaml.representer.RepresenterError: cannot represent an object: <slot wrapper '__abs__' of 'int' objects> 

Toute solution qui me permet de (en toute sécurité) sauvegarder uniquement le type (j'ai besoin de générer des objets du type ET vérifier si un objet entrant est d'un certain type) serait apprécié. Une fonction ou une classe qui génère mon type requis aurait le même problème de ne pas être sérialisable non plus.


P.S. J'ai aussi peut-être trouvé un bogue, où l'analyseur aura, pour une raison quelconque, un comportement différent selon que le même argument efficace a été (tenté) d'être sérialisé.

Y.dump(A(str), sys.stdout) 
Y.dump(A(str), sys.stdout) 
Y.dump(A(str), sys.stdout) 
Y.dump(A(str), sys.stdout) 

Sorties:

>>> Y.dump(A(str), sys.stdout) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\main.py", line 352, in dump 
    return self.dump_all([data], stream, _kw, transform=transform) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\main.py", line 383, in dump_all 
    self.representer.represent(data) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 73, in represent 
    node = self.represent_data(data) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 101, in represent_data 
    node = self.yaml_representers[data_types[0]](self, data) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\main.py", line 552, in t_y 
    tag, data, cls, flow_style=representer.default_flow_style) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 371, in represent_yaml_object 
    return self.represent_mapping(tag, state, flow_style=flow_style) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 206, in represent_mapping 
    node_value = self.represent_data(item_value) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 101, in represent_data 
    node = self.yaml_representers[data_types[0]](self, data) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\main.py", line 492, in t_y 
    tag, data, cls, flow_style=representer.default_flow_style) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 371, in represent_yaml_object 
    return self.represent_mapping(tag, state, flow_style=flow_style) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 206, in represent_mapping 
    node_value = self.represent_data(item_value) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 111, in represent_data 
    node = self.yaml_representers[None](self, data) 
    File "C:\Program Files\Anaconda3\lib\site-packages\ruamel\yaml\representer.py", line 375, in represent_undefined 
    raise RepresenterError("cannot represent an object: %s" % data) 
ruamel.yaml.representer.RepresenterError: cannot represent an object: <slot wrapper '__add__' of 'str' objects> 
>>> Y.dump(A(str), sys.stdout) 
!Aclass 
type: !type {} 
>>> Y.dump(A(str), sys.stdout) 
Traceback (most recent call last): 
# same traceback here 
ruamel.yaml.representer.RepresenterError: cannot represent an object: <slot wrapper '__add__' of 'str' objects> 
>>> Y.dump(A(str), sys.stdout) 
!Aclass 
type: !type {} 
>>> 

Répondre

1

YAML prévoit de jeter des objets, et ne finit par en écrivant des chaînes scalaires. T1 n'est pas un objet (ni T2 ou T3), et c'est de là que vient le problème. Vous pouvez essayer de faire chaque référence de classe dans un objet et utilise des étiquettes sur ceux-ci, mais que l'OMI ne fait que compliquer les choses.

Finalement, tout se résume à obtenir une représentation scalaire, soit une représentation de chaîne de la classe dans le fichier, vous pourriez aussi bien adapter A() pour vider directement une représentation de chaîne et relire:

import sys 
from ruamel.yaml import YAML, yaml_object 
from ruamel.yaml.compat import StringIO 
from ruamel.yaml.scalarstring import DoubleQuotedScalarString 


Y = YAML(typ="safe", pure=True) 

# ============== 

@yaml_object(Y) 
class A(object): 
    """Object I want to serialize""" 
    yaml_tag = "!Aclass" 
    def __init__(self, type): 
     self.type = type #.__class__.__name__ 

    @classmethod 
    def to_yaml(cls, representer, node): 
     return representer.represent_scalar(
      cls.yaml_tag, u'{}'.format(node.type.__name__) 
     ) 

    @classmethod 
    def from_yaml(cls, constructor, node): 
     if '.' in node.value: # in some other module 
      m, n = node.value.rsplit('.', 1) 
      return cls(getattr(sys.modules[m], n)) 
     else: 
      return cls(globals()[node.value]) 


class T1(object): 
    """This will be referenced.""" 
    pass 


@yaml_object(Y) 
class T2(object): 
    """Another referenced object""" 
    pass 


class T3(object): 
    """Yet another try""" 
    pass 
Y.register_class(T3) 


for t in T1, T2, T3, DoubleQuotedScalarString: 
    print('----------------------') 
    x = StringIO() 
    s = A(t) 
    print('s', s.type) 
    Y.dump(s, x) 
    print(x.getvalue()) 

    d = Y.load(x.getvalue()) 
    print('d', d.type) 

qui donne:

---------------------- 
s <class '__main__.T1'> 
!Aclass T1 
... 

d <class '__main__.T1'> 
---------------------- 
s <class '__main__.T2'> 
!Aclass T2 
... 

d <class '__main__.T2'> 
---------------------- 
s <class '__main__.T3'> 
!Aclass T3 
... 

d <class '__main__.T3'> 
---------------------- 
s <class 'ruamel.yaml.scalarstring.DoubleQuotedScalarString'> 
!Aclass DoubleQuotedScalarString 
... 

d <class 'ruamel.yaml.scalarstring.DoubleQuotedScalarString'> 

S'il y a d'autres attributs sur A() qui doit être sous-évaluées/chargé, vous devez créer un dictionnaire (avec la chaîne convertie .type) et de vidage/l oad ça.

Je ne pense pas que vous ayez trouvé un vrai bogue, mais que vous subissiez un effet secondaire suite à une erreur: l'objet Y (et ses composants) sont laissés dans un état indéfini. Vous ne devez pas réutiliser une instance YAML() après avoir détecté des erreurs. Cela devrait être plus clair dans la documentation. Donc, si vous voulez faire un try/except dans la boucle for, vous devez déplacer le Y = YAML(typ='safe', pure=True) dans la partie try.

+0

Très bien, cela semble fonctionner, si je veux garder, par exemple, T2 dans mon. Cependant, puisque je construis des objets de la classe donnée, et que Y ne peut pas dire (du moins pas à partir de votre implémentation A) s'il est sûr de charger l'objet, je peux charger un type malveillant dans mon code. Dans ce cas, T1 était "chargé" même si la classe n'était pas enregistrée. Une solution possible serait de vérifier dans A.from_yaml() si la classe trouvée a été enregistrée par Y (c'est-à-dire dans constructor.yaml_constructors?), Je suppose. En ce qui concerne la deuxième partie, cela a du sens. –

+0

Vous avez raison à propos de la sécurité, j'ai réfléchi à cela en travaillant sur la réponse, mais j'ai oublié de l'inclure. Je voudrais mettre tous les types pertinents dans un module (ou plusieurs modules dans un sous-répertoire) et les importer à partir de là. Ensuite, vous pouvez vérifier la chaîne dans 'node.value' dans' A' 'from_yaml()'. Vous pouvez également créer votre propre @ @ yaml_type qui définit un attribut (unique) sur lequel vous testez (au moment du vidage et/ou du chargement). Il n'est pas nécessaire de réutiliser l'enregistrement des objets par ruamel.yaml (ce qui est essentiellement autre chose). – Anthon

+0

J'ai fait une petite maquette dans ma réponse, n'hésitez pas à la modifier (ou votre propre réponse) pour la mettre en sécurité. EDIT: J'ai manqué ton commentaire en postant, pas très sûr de ce que tu veux dire par là? Voulez-vous dire utiliser un décorateur @yaml_type pour définir un attribut caché dans un objet? Qu'est-ce qui empêche quelqu'un de faire la même chose à sa propre classe et d'imiter le nôtre? –

0

En ajoutant Anthon's answer, j'ai commencé à modifier A.from_yaml pour être plus sûr, même si je n'ai pas parcouru tous les cas pour _check_registered(). L'idée est de charger tous les types que Y permettrait de charger des instances de, et d'empêcher tous les autres types.Considérez ceci comme un WIP:

import sys 
from ruamel.yaml import YAML, yaml_object 
from ruamel.yaml.compat import StringIO 
from ruamel.yaml.scalarstring import DoubleQuotedScalarString 


Y = YAML(typ="safe", pure=True) 

# ============== 

@yaml_object(Y) 
class A(object): 
    """Object I want to serialize""" 
    yaml_tag = "!Aclass" 
    def __init__(self, type): 
     self.type = type #.__class__.__name__ 

    @classmethod 
    def to_yaml(cls, representer, node): 
     return representer.represent_scalar(
      cls.yaml_tag, u'{}'.format(node.type.__name__) 
     ) 

    @classmethod 
    def from_yaml(cls, constructor, node): 
     if '.' in node.value: # in some other module 
      m, n = node.value.rsplit('.', 1) 
      t = getattr(sys.modules[m], n) 
     else: 
      t = globals()[node.value] 
     cls._check_registered(t,constructor, node) 
     return cls(t) 

    @classmethod 
    def _check_registered(cls, t, constructor, node): 
     # Check if type "t" is registered in "constr" 
     # Note: only a very basic check, 
     # and ideally should be made more secure 

     if hasattr(t,"yaml_tag"): 
      if t.yaml_tag in constructor.yaml_constructors: 

       return 
      raise Exception("Error: Tag not registered!") 
     else: 
      # 
      raise Exception("Error: No attribute 'yaml_tag'!") 
     pass 

    pass 

class T1(object): 
    """This will be referenced.""" 
    yaml_tag = u"!T1" 
    pass 


@yaml_object(Y) 
class T2(object): 
    """Another referenced object""" 
    yaml_tag = u"!T2" 

    def __init__(self): 
     print("Initializing...") 
     pass 
    pass 

class T2_bad(object): 
    """Malicious class impersonating T2""" 
    # Note: It's not registered 
    yaml_tag = u"!T2" 

    def __init__(self): 
     print("Evil code here!") 
     pass 

    pass 


class T3(object): 
    """Yet another try""" 
    yaml_tag = u"!T3" 
    pass 
Y.register_class(T3) 



for t in T1, T2, T2_bad, T3, DoubleQuotedScalarString: 
    try: 
     print('----------------------') 
     x = StringIO() 
     s = A(t) 
     print('s', s.type) 
     Y.dump(s, x) 
     print(x.getvalue()) 
     d = Y.load(x.getvalue()) 
     print('d', d.type) 
     d.type() 
    except Exception as e: 
     print(e) 
     continue 
    pass 

Ce retourne:, elle ne permet pas les types sans yaml_tag défini

---------------------- 
s <class '__main__.T1'> 
!Aclass T1 
... 

Error: Tag not registered! 
---------------------- 
s <class '__main__.T2'> 
!Aclass T2 
... 

d <class '__main__.T2'> 
Initializing... 
<__main__.T2 object at 0x0000015B8EC82F60> 
---------------------- 
s <class '__main__.T2_bad'> 
!Aclass T2_bad 
... 

d <class '__main__.T2_bad'> 
Evil code here! 
<__main__.T2_bad object at 0x0000015B8EC82EF0> 
---------------------- 
s <class '__main__.T3'> 
!Aclass T3 
... 

d <class '__main__.T3'> 
<__main__.T3 object at 0x0000015B8EC82E10> 
---------------------- 
s <class 'ruamel.yaml.scalarstring.DoubleQuotedScalarString'> 
!Aclass DoubleQuotedScalarString 
... 

Error: No attribute 'yaml_tag'! 

Comme on le voit, il est toujours pas sécurisé (a été exécuté "Code Evil"). N'hésitez pas à modifier pour résoudre ce problème.