views:

50

answers:

3

I want to selectively hide some resources based on some form of authentication in web.py, but their existence is revealed by 405 responses to any HTTP method that I haven't implemented.

Here's an example:

import web

urls = (
    '/secret', 'secret',
    )

app = web.application(urls, globals())

class secret():
    def GET(self):
        if web.cookies().get('password') == 'secretpassword':
            return "Dastardly secret plans..."
        raise web.notfound()

if __name__ == "__main__":
    app.run()

When an undefined method request is issued, the resource is revealed:

$ curl -v -X DELETE http://localhost:8080/secret
...
> DELETE /secret HTTP/1.1
...
< HTTP/1.1 405 Method Not Allowed
< Content-Type: text/html
< Allow: GET
...

I could implement the same check for the other common methods in the HTTP specification, but a creative miscreant might invent their own:

$ curl -v -X SHENANIGANS http://localhost:8080/secret
...
> SHENANIGANS /secret HTTP/1.1
...
< HTTP/1.1 405 Method Not Allowed
< Content-Type: text/html
< Allow: GET
...

Is there a way to implement a catch all method in a web.py class for any HTTP method, so I can ensure the security check will be run?

Or is there an alternative way to hide these resources?

A: 

you can define any method in your 'secret' class, such as DELETE or SHENANIGANS, like this:

class secret():

    def DELETE(self):
       ...

    def SHENANIGANS(self):
       ...
Xie Yanbo
An attacker can invent whatever method name they like. If I start defining methods for every possibility I'm gonna miss my deadline :)
Ian Mackinnon
+1  A: 

You can implement handle-all-methods method like this:

class HelloType(type):
    """Metaclass is needed to fool hasattr(cls, method) check"""
    def __getattribute__(obj, name):
        try:
            return object.__getattribute__(obj, name)
        except AttributeError:
            return object.__getattribute__(obj, '_handle_unknown')        

class hello(object):
    __metaclass__ = HelloType
    def GET(self, *args, **kw):
        if web.cookies().get('password') == 'secretpassword':
            return "Dastardly secret plans..."
        raise web.notfound()

    def _handle_unknown(self, *args, **kw):
        """This method will be called for all requests, which have no defined method"""
        raise web.notfound()

    def __getattribute__(obj, name):
        try:
            return object.__getattribute__(obj, name)
        except AttributeError:
            return object.__getattribute__(obj, '_handle_unknown') 

__getattribute__ is implemented twice due to the way web.py checks for method existence:

def _delegate(self, f, fvars, args=[]):
    def handle_class(cls):
        meth = web.ctx.method
        if meth == 'HEAD' and not hasattr(cls, meth):
            meth = 'GET'
        if not hasattr(cls, meth): # Calls type's __getattribute__
            raise web.nomethod(cls)
        tocall = getattr(cls(), meth) # Calls instance's __getattribute__
Daniel Kluev
Thanks so much for this clear and helpful explanation. I decided to used a different method that I felt more comfortable with, but I had no idea where to start before reading your answer!
Ian Mackinnon
+1  A: 

Enlightened by Daniel Kluev's answer, I ended up deriving from web.application to add support for a default method in the _delegate method:

import types

class application(web.application):
    def _delegate(self, f, fvars, args=[]):
        def handle_class(cls):
            meth = web.ctx.method
            if meth == 'HEAD' and not hasattr(cls, meth):
                meth = 'GET'
            if not hasattr(cls, meth):
                if hasattr(cls, '_default'):
                    tocall = getattr(cls(), '_default')
                    return tocall(*args)
                raise web.nomethod(cls)
            tocall = getattr(cls(), meth)
            return tocall(*args)

        def is_class(o): return isinstance(o, (types.ClassType, type))
        ...

Instantiation:

app = application(urls, globals())

Page class:

class secret():
    def _default(self):
        raise web.notfound()

    def GET(self):
        ...

I prefer this solution because it keeps the page classes clean and affords further customisation of the delegation process in a single place. For example, another feature I wanted was transparent overloaded POST (eg. redirecting a POST request with method=DELETE to the DELETE method of the page class) and it's simple to add that here too:

            ...
            meth = web.ctx.method
            if meth == 'POST' and 'method' in web.input():
                meth = web.input()['method']
            ...
Ian Mackinnon