上周 RealWorld CTF 2018 web 题 bookhub 有个未授权访问的漏洞,比较有意思,赛后看了一下公开的 WriteUp,大家也都没写清楚,所以就有了这篇博文。
前言
这个题是用 flask 框架写的,在 www/bookhub/views/user.py
中,refresh_session
方法存在未授权访问漏洞,代码是这样写的:
1 | @login_required |
注意看 @login_required
这个装饰器写在了 route
装饰器上面了,导致了 login_required
未调用。那么,为什么会这样子呢?
官方文档
Flask 官方文档中关于Login Required Decorator说明这一节里面有一行说明:
1 | To use the decorator, apply it as innermost decorator to a view function. When applying further decorators, always remember that the route() decorator is the outermost. |
大概意思就是,必须保证
route
装饰器在最顶层
那么为什么要这样提示呢?
Python 装饰器顺序说明
本节内容可直接参考:Python 装饰器执行顺序迷思
总结一下就是,装饰的顺序按靠近函数顺序执行,从内到外装饰,调用时由外而内,执行顺序和装饰顺序相反。
回过头来看 Flask
Flask 框架中,route
装饰器是这么写的:
1 | def route(self, rule, **options): |
route
调用了 add_url_rule
, 对传入的 f
添加一条 URL 规则。
所以,按照 python 装饰器顺序:
- 如果
@app.route
在内层,那么就会把最原始的 view 函数传给add_url_rule
, Flask 框架就会添加一条 URL 规则,指向最原始的 view 函数。 - 如果
@app.route
在外层,那么就会把已经被login_required
装饰过的 view 函数传给add_url_rule
, Flask 框架就会添加一条 URL 规则,指向已经装饰过的 view 函数。
下面是两个例子,来说明:
正确写法
1 | @user_blueprint.route('/admin/refresh_session/', methods=['POST']) |
这段代码相当于:
1 | # 这里没有装饰器 |
/admin/refresh_session/
这条路由指向的实际是login_wrapped
,当访问/admin/refresh_session
时,会调用login_wrapped
,接着再调用refresh_session
。这样就经过了认证检查。
错误写法
1 | @login_required |
这段代码相当于:
1 | # 这里没有装饰器 |
/admin/refresh_session/
这条路由指向的实际是refresh_session
, 当访问/admin/refresh_session
时,会调用refresh_session
函数,而login_wrapped
并没有与路由挂勾,所以不会被调用。