本文关注一个非常简单的问题: 何时进行 urldecode(unquote)。所涉及的代码分别为:
- Flask 0.12.2, commit sha
571334df8e26333f34873a3dcb84441946e6c64c
- Werkzeug 0.14, commit sha
5b53d1539147c5db3210e0769d85397ab91f902d
- Gunicorn 19.7.1, commit sha
328e509260ae70de6c04c5ba885ee17960b3ced5
以 Flask + Gunicorn 为例(本人 Gunicorn 比 Werkzeug 熟一些)
app = Flask(__name__)
@app.route('/<name>/')
def demo(name):
from_ = request.args.get('from', '')
return f"I'm {name} from {from_}."
if __name__ == '__main__':
app.run()
$ curl "127.0.0.1:8000/%e8%b7%af%e6%98%8e%e9%9d%9e/?from=%e5%8d%a1%e5%a1%9e%e5%b0%94%e5%ad%a6%e9%99%a2"
I'm 路明非 from 卡塞尔学院.
我们可以看到在 Flask 中,得到的已经是解码之后的字符串了,那么我们便来分析一下解码发生的位置。首先需要说明的是 PATH_INFO
和 QUERY_STRING
是在不同的位置进行的解码
PATH_INFO
#
# 简化后代码
def handle_request(self, listener, req, client, addr):
resp, environ = wsgi.create(req, client, addr, listener.getsockname(), self.cfg)
respiter = self.wsgi(environ, resp.start_response)
for item in respiter:
resp.write(item)
resp.close()
environ
是通过 wsgi.create
所解析出来,gunicorn/http/wsgi.py#L116
def create(req, sock, client, server, cfg):
# ...
environ['PATH_INFO'] = unquote_to_wsgi_str(path_info)
# ...
这里对 PATH_INFO
进行了解码,但是在 PEP-333 中并未看到需要对此字段进行解码的要求 environ Variables。并且翻了 werkzeug 的代码也是对 PATH_INFO
进行了解码,可以参考这里
关于需不需要进行解码,Gunicorn
上也有相关讨论
QUERY_STRING
#
再来看 QUERY_STRING
的解码,Flask 的 Request
是继承的 werkzeug.wrappers.BaseRequest
。稍微来回顾一下这个流程
# flask/app.py
from .wrappers import Request
class Flask(_PackageBoundObject):
request_class = Request
def request_context(self, environ):
return RequestContext(self, environ)
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
# ...
def __call__(self, environ, start_response):
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response)
# flask/ctx.py
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
核心实现位于 werkzeug/wrappers.py#L453
# werkzeug/wrappers.py
class BaseRequest(object):
@cached_property
def args(self):
return url_decode(wsgi_get_bytes(self.environ.get('QUERY_STRING', '')),
self.url_charset, errors=self.encoding_errors,
cls=self.parameter_storage_class)
这里对于 QUERY_STRING
进行了解码。那么如果是以 POST 提交 Content-Type: application/x-www-form-urlencoded
格式的数据,根据协议规定自然会进行解码,这个也是 Web Framework 所需要做的
题外话:PHP是世界上最好的语言
<?php
if(eregi("hackerDJ",$_GET[id])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}
?>
$_GET[id]
会自动进行 urldecode,所以编码两次就行了 %2568ackerDJ
=> %68ackerDJ
=> hackerDJ
Summary up#
PATH_INFO
应当有 WSGI Server 进行解码QUERY_STRING
应当有框架自身进行解码