2

Let's say I have an object that is a property of another object.

import Pyro4

@Pyro4.expose
class ClassB:
    def foo(self):
        return 'Hello from ClassB::foo()'

@Pyro4.expose
class ClassA:
    def __init__(self):
        self._b = ClassB()

    def foo(self):
        return 'Hello from ClassA::foo()'

    @property
    def b(self):
        return self._b

if __name__ == '__main__':
    daemon = Pyro4.Daemon(host='localhost')

    ns = Pyro4.locateNS(host='localhost', port=9090)
    ns.register('ClassA', daemon.register(ClassA))

    daemon.requestLoop()

And on the local machine, I run this

import Pyro4

if __name__ == '__main__':
    ns = Pyro4.locateNS(host='localhost', port=9090)
    uri = ns.lookup('ClassA')
    a = Pyro4.Proxy(uri)

    print(a.foo())
    print(a.b.foo()) # Fails here

When I try to invoke a.b.foo, it's trying to serialize a.b and invoke foo on it locally, but I want to invoke foo on the instance of ClassB that already exists on the remote ClassA instance.

Of course, I could add a method to ClassA that delegates to b.foo(), e.g.

def classA:
    # ...
    def foo_on_b(self):
        return self._b.foo()

But I'd rather not do that.

Caius Cosades
  • 33
  • 1
  • 9

2 Answers2

1

This is not possible, by design. It's a security vulnerability. https://pyro4.readthedocs.io/en/stable/security.html#dotted-names-object-traversal

Irmen de Jong
  • 2,739
  • 1
  • 14
  • 26
0

Managed to hack it like this.

remote.py

import Pyro4

class C:
    def __init__(self):
        self.val = 123

class B:
    def __init__(self):
        self.c = C()

class A:
    def __init__(self):
        self.b = B()

def getattr_r(obj, *attrs):
    return getattr_r(getattr(obj, attrs[0]), *attrs[1:]) if attrs else obj

class ClassC:
    def foo(self, x, y):
        return 'Hello from ClassC::foo()! {} + {} = {}'.format(x, y, x + y)

class ClassB:
    def __init__(self):
        self.c = ClassC()

    def foo(self, x, y):
        return 'Hello from ClassB::foo()! {} + {} = {}'.format(x, y, x + y)

@Pyro4.expose
class ClassA:
    def __init__(self):
        self.b = ClassB()

    def foo(self, x, y):
        return 'Hello from ClassA::foo()! {} + {} = {}'.format(x, y, x + y)

    def invoke_on_subobj(self, obj_chain, *args, **kwargs):   
        return getattr_r(self, *obj_chain)(*args, **kwargs)

if __name__ == '__main__':
    daemon = Pyro4.Daemon(host='localhost')

    ns = Pyro4.locateNS(host='localhost', port=9090)
    ns.register('ClassA', daemon.register(ClassA))

    daemon.requestLoop()

local.py

import Pyro4

class ObjTraversableProxy:
    def __init__(self, proxy, bound_attrs=[]):
        self._proxy = proxy
        self._bound_attrs = bound_attrs

    def __getattr__(self, attr):
        return ObjTraversableProxy(self._proxy, self._bound_attrs + [attr])

    def __call__(self, *args, **kwargs):
        if len(self._bound_attrs) > 1:
            return self._proxy.invoke_on_subobj(self._bound_attrs, *args, **kwargs)
        else:
            return getattr(self._proxy, self._bound_attrs[0])(*args, **kwargs)

if __name__ == '__main__':
    ns = Pyro4.locateNS(host='localhost', port=9090)
    uri = ns.lookup('ClassA')
    a = ObjTraversableProxy(Pyro4.Proxy(uri))

    print(a.foo(3, 4))
    print(a.b.foo(5, 6))
    print(a.b.c.foo(6, 7))

Result

Hello from ClassA::foo()! 3 + 4 = 7
Hello from ClassB::foo()! 5 + 6 = 11
Hello from ClassC::foo()! 6 + 7 = 13
Caius Cosades
  • 33
  • 1
  • 9
  • are you sure you didn't just re-introduce the security vulnerability that is being talked about. Remember, if you allow arbitrary object traversals you expose access to all kinds of properties and methods of your base classes, up to and including im_func.func_globals.update() – Irmen de Jong Mar 07 '19 at 23:10
  • I expect I have, but we're just using Pyro on our internal network to drive our automated tests, so security isn't a big concern. – Caius Cosades Mar 08 '19 at 11:51