-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
__call__ always accessible, even when not exposed by _rpyc_getattr #239
Comments
Hi, I see your concern.
Please go ahead! Thanks.
For the future: it's better to post in plain text to reduce the trouble for others to go through downloading and extracting before they can see a few lines of code... which will drastically reduce the likelihood of others reading your issue or finding related problems via search engines (and is also harder to backup/migrate to an external system and unsafer in case the files will be deleted at some point). For reference, the extracted code is: import unittest
from rpyc.utils.server import ThreadedServer
import rpyc
import threading
class TestService(rpyc.Service):
def _rpyc_getattr(self, name):
raise AttributeError()
def __call__(self):
print("OH NO")
def hidden(self):
print("Can't get here")
def test_simple_client_server(): #tests family_exclude and preference order too
port=18861
#THIS SHOULD BLOCK EVERYTHING NOT EXPOSED BY _rpyc_getattr
config={ "allow_safe_attrs":False,
"allow_exposed_attrs":False,
"allow_getattr":False }
server=ThreadedServer( TestService, hostname="localhost", port=port, protocol_config=config )
serverThread = threading.Thread( target=server.start, args=())
serverThread.start()
connection=rpyc.connect("localhost" , port)
root=connection.root
root.__call__() #This should not work
root() #in an ideal world this shouldn't work either.
connection.close()
server.close()
serverThread.join()
if __name__=="__main__":
test_simple_client_server() |
Fixed call accessI have made a fork with the necessary changes: https://github.com/seveirein/rpyc I have written some unit test cases and tested under Python 2.7.13 and 3.5.3. ProblemsThis isn't easy to fix. Every callable used over a rpyc connection comes from a netref. Let's say you want to call Even though there is a call attribute protocol message, it is rarely used, because of how the underlying Python works. That means at the time of a call, the protocol handler doesn't know whether you are holding an item that was legitimately gotten from a netref in a way conducive with the security policy set, or some random callable object that has been gotten another way (say via being returned as an object). This means that verifying callables were reached appropriately will by necessity be a bit hackish. Accessing callsI've implemented an Accessibility Case 1If It will critically not work however for all methods of the class. They are callables, that have their own Accessibility Case 2If the method is wrapped/bound, you can backwards verify that its parent self/im_self has a reference to it by callable.name. This won't work on functions and static methods at all, and will not work if you do something like: def foo(self):
pass
class a:
x=foo
Other Necessary ChangesTwo other changes were necessary of some importance.
|
Can you submit a PR? (doesn't have to be final) I find it easier to reason about code than words. Best, Thomas |
PR #241 It passes the unit tests and adds its own. |
So, I was thinking about this some more, and it might be possible to do this much more cleanly by exposing the needed binding information when a netref to the function is made. It would require a slight modification to the magic in netref code. |
Any reason not to simply check for accessibility of |
it's not easy to see why it doesn't work till you try it. To reiterate from my previous post: "Let's say you want to call myNetref.foo() -- this typically is broken down into two protocol steps---getting foo, and calling it." The bound "foo" has its own oid. So it is impossible to check simply at If you check at call time, you suddenly find that you can't use any valid dereferenced methods whatsoever, because you'll end up checking every method for rpyc_getattr or exposed__call_ or whatever..and it is just a method. |
So, my feeling on this for the moment is to not integrate my pull request till further notice, but leave the issue open. I'm working on some stuff that may make it a moot point anyways. That said, I may not get back to it for a few weeks. |
So PR #243 is my current solution to this. With the old system there's no good way to protect this. In the old system, once you have an OID you have to be able to call it without checking anything, or otherwise you can't call most things. With the |
Well, PR #243 is dead, and I still want to address the bug discussed her in the context of RPyC by itself. Is it okay if I open up discussion on the Google Group to discuss several potential fixes? In the meantime--here is a recap of the problem: You can invoke The difficulty in fixing the problem cleanly lies in that methods and functions themselves are objects. When you get a netref to a method or a function, you want to be able to call that netref directly. In order to secure the system you need to make some distinction about what objects should be callable, and which should not. At the time you need to make that distinction, the origin of where that object came from is not available. I've come up with some potential methods for fixing it. Method AThe strategy for method A is really simple. If you have a netref to a method or a function, you probably should be able to call it. Things only really get muddy with classes and callable instances. Therefore, we can add
This mostly works, but there is one really ugly issue::
The cpython type 'method-wrapper' is a largely undocumented type, that does not exist in the The other major drawback of this method is that it is very fixed--it doesn't allow a per object policy. Method BThe strategy for method B is to keep things even simpler. Every time there is an attempt to This sorta punts the problem into the user's problem space, rather than ours. That's good from a standpoint of them having control of the problem. They do however, still have very little information to make their decisions on. Method CRequire all function/method objects to somehow be marked, modified, or registered explicitly to be callable. How remains to be discussed. Currently the use of determining accessibility by name is not
Method C is what I used for the monster patch, as everything exposed was essentially a specially wrapped type. The main drawbacks of Method C are that even a much simpler implementation than I did would be complicated, and would require the use of a different way of exposing stuff than currently used by RPyC. |
Hi, I will summarize my thoughts on the topic. My first thoughts were similar to what you described in A: In However, I don't really think this is a clean solution, since it acts as sort of a global behaviour that is not easy to configure on individual methods etc. It is too restricted in what it does allow. After more thinking, I've come to the conclusion that this responsibility should be left to the user entirely if they desire (so your extensive patches could be implemented easily as extension). I think the main problem is that it is right now hard to hook into the getattr mechanism from the outside. Theoretically, you can do it by monkey patching like so: class MyConnection(rpyc.Connection):
def _handle_call(self, oid, args, kwargs=()):
# ... can check here if this is allowed however you want..
return self._local_objects[oid](*args, **dict(kwargs))
# also need to update _HANDLERS here..
# monkey-patch rpyc.server in order to make ThreadedServer use our subclass:
rpyc.server.Connection = MyConnection Of course, that's hackish and introduces a global change, so I can't recommend it to anyone. However, the same can be achieved with just a couple of lines changed in the code by adding a Next step: you probably don't want to subclass This should allow the user to have full control with only a couple of lines changed. What do you think? |
Integrating PR #245 should close this issue. |
This release brings a few minor backward incompatibilities, so be sure to read on before upgrading. However, fear not: the ones that are most likely relevant to you have a relatively simple migration path. Backward Incompatibilities ^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``classic.teleport_function`` now executes the function in the connection's namespace by default. To get the old behaviour, use ``teleport_function(conn, func, conn.modules[func.__module__].__dict__)`` instead. * Changed signature of ``Service.on_connect`` and ``on_disconnect``, adding the connection as argument. * Changed signature of ``Service.__init__``, removing the connection argument * no longer store connection as ``self._conn``. (allows services that serve multiple clients using the same service object, see `#198`_). * ``SlaveService`` is now split into two asymetric classes: ``SlaveService`` and ``MasterService``. The slave exposes functionality to the master but can not anymore access remote objects on the master (`#232`_, `#248`_). If you were previously using ``SlaveService``, you may experience problems when feeding the slave with netrefs to objects on the master. In this case, do any of the following: * use ``ClassicService`` (acts exactly like the old ``SlaveService``) * use ``SlaveService`` with a ``config`` that allows attribute access etc * use ``rpyc.utils.deliver`` to feed copies rather than netrefs to the slave * ``RegistryServer.on_service_removed`` is once again called whenever a service instance is removed, making it symmetric to ``on_service_added`` (`#238`_) This reverts PR `#173`_ on issue `#172`_. * Removed module ``rpyc.experimental.splitbrain``. It's too confusing and undocumented for me and I won't be developing it, so better remove it altogether. (It's still available in the ``splitbrain`` branch) * Removed module ``rpyc.experimental.retunnel``. Seemingly unused anywhere, no documentation, no clue what this is about. * ``bin/rpyc_classic.py`` will bind to ``127.0.0.1`` instead of ``0.0.0.0`` by default * ``SlaveService`` no longer serves exposed attributes (i.e., it now uses ``allow_exposed_attrs=False``) * Exposed attributes no longer hide plain attributes if one otherwise has the required permissions to access the plain attribute. (`#165`_) .. _#165: #165 .. _#172: #172 .. _#173: #173 .. _#198: #198 .. _#232: #232 .. _#238: #238 .. _#248: #248 What else is new ^^^^^^^^^^^^^^^^ * teleported functions will now be defined by default in the globals dict * Can now explicitly specify globals for teleported functions * Can now use streams as context manager * keep a hard reference to connection in netrefs, may fix some ``EOFError`` issues, in particular on Jython related (`#237`_) * handle synchronous and asynchronous requests uniformly * fix deadlock with connections talking to each other multithreadedly (`#270`_) * handle timeouts cumulatively * fix possible performance bug in ``Win32PipeStream.poll`` (oversleeping) * use readthedocs theme for documentation (`#269`_) * actually time out sync requests (`#264`_) * clarify documentation concerning exceptions in ``Connection.ping`` (`#265`_) * fix ``__hash__`` for netrefs (`#267`_, `#268`_) * rename ``async`` module to ``async_`` for py37 compatibility (`#253`_) * fix ``deliver()`` from IronPython to CPython2 (`#251`_) * fix brine string handling in py2 IronPython (`#251`_) * add gevent_ Server. For now, this requires using ``gevent.monkey.patch_all()`` before importing for rpyc. Client connections can already be made without further changes to rpyc, just using gevent's monkey patching. (`#146`_) * add function ``rpyc.lib.spawn`` to spawn daemon threads * fix several bugs in ``bin/rpycd.py`` that crashed this script on startup (`#231`_) * fix problem with MongoDB, or more generally any remote objects that have a *catch-all* ``__getattr__`` (`#165`_) * fix bug when copying remote numpy arrays (`#236`_) * added ``rpyc.utils.helpers.classpartial`` to bind arguments to services (`#244`_) * can now pass services optionally as instance or class (could only pass as class, `#244`_) * The service is now charged with setting up the connection, doing so in ``Service._connect``. This allows using custom protocols by e.g. subclassing ``Connection``. More discussions and related features in `#239`_-`#247`_. * service can now easily override protocol handlers, by updating ``conn._HANDLERS`` in ``_connect`` or ``on_connect``. For example: ``conn._HANDLERS[HANDLE_GETATTR] = self._handle_getattr``. * most protocol handlers (``Connection._handle_XXX``) now directly get the object rather than its ID as first argument. This makes overriding individual handlers feel much more high-level. And by the way it turns out that this fixes two long-standing issues (`#137`_, `#153`_) * fix bug with proxying context managers (`#228`_) * expose server classes from ``rpyc`` top level module * fix logger issue on jython .. _#137: #137 .. _#146: #146 .. _#153: #153 .. _#165: #165 .. _#228: #228 .. _#231: #231 .. _#236: #236 .. _#237: #237 .. _#239: #239 .. _#244: #244 .. _#247: #247 .. _#251: #251 .. _#253: #253 .. _#264: #264 .. _#265: #265 .. _#267: #267 .. _#268: #268 .. _#269: #269 .. _#270: #270 .. _gevent: http://www.gevent.org/
RPYC has no mechanism to preclude access to the python
__call__
magic method.Fetching a
__call__
via__getattribute__
on a netref fetches the local netref__call__
function automatically:https://github.com/tomerfiliba/rpyc/blame/master/rpyc/core/netref.py#L148
Then the
__call__
method does asyncreq(_self, consts.HANDLE_CALL, args, kwargs)
directly.https://github.com/tomerfiliba/rpyc/blame/master/rpyc/core/netref.py#L197
This is sent on through the protocol, and there is no checking on either side if the remote
__call__
attribute was exposed by_rpyc_getattr
.Simple test case attached:
rpyc__call__issue.zip
Users of restricted() may get a nasty surprise. As for my own purposes, I am using rpyc and I have created a lot of tools and magic python decorators (which I may want to contribute back to rpyc) to describe security restrictions.
I have RPYC configured so it doesn't allow ANYTHING to be accessed except via
_rpyc_getattr
. I was hoping that would include the__call__
's on class instances, classes themselves, and even function/method references. If there is no way enforce this, I will need to create my own patched fork of rpyc to meet my security model constraints.Since this 'hole" is longstanding, I think the best way to address it is with a new
protocol_config
flag that locks down all calling access (unless the__call__
attribute is exposed by_rpyc_getattr
).I can create a patch myself, but want some feedback from maintainers before I go off and do that.
The text was updated successfully, but these errors were encountered: