Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

web.py: How to selectively hide resources with 404s for any HTTP method?

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?

like image 434
Ian Mackinnon Avatar asked Oct 14 '22 02:10

Ian Mackinnon


1 Answers

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']
            ...
like image 150
Ian Mackinnon Avatar answered Nov 02 '22 22:11

Ian Mackinnon