Werkzeug: Werkzeug 错误地处理多行标题

创建于 2017-03-10  ·  8评论  ·  资料来源: pallets/werkzeug

根据RFC 2616

如果续行以空格或水平制表符开头,则 HTTP/1.1 标头字段值可以折叠成多行。 所有线性空白,包括折叠,都具有与 SP 相同的语义。 在解释字段值或向下游转发消息之前,接收者可以用单个 SP 替换任何线性空白。

但是,werkzeug 不接受带有换行符的标头值,即使它们遵守此约定。

>>> import werkzeug
>>> werkzeug.Headers().add('foo', 'bar\n baz')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../venv/local/lib/python2.7/site-packages/werkzeug/datastructures.py", line 1136, in add
    self._validate_value(_value)
  File ".../venv/local/lib/python2.7/site-packages/werkzeug/datastructures.py", line 1143, in _validate_value
    raise ValueError('Detected newline in header value.  This is '
ValueError: Detected newline in header value.  This is a potential security problem

此外,此限制的应用不一致。

>>> werkzeug.Headers([('foo', 'bar\n baz')])
Headers([('foo', 'bar\n baz')])

我在尝试编写与通过标头转发客户端证书的 nginx 相关的测试用例时遇到了这个问题,因此有一个真正的用例来正确支持它。

bug server

最有用的评论

@davidism正如我在之前的评论中提到的,这里实际上有两个错误,当前的主分支上都没有修复。


第一个错误涉及 werkzeug 开发服务器如何处理换行的标头。 可以使用以下服务器代码重现它,该代码打印X-Example标头的值:

from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response

def app(environ, start_response):
    request = Request(environ)
    print(repr(request.headers.get('X-Example')))
    response = Response(status=204)
    return response(environ, start_response)

run_simple('localhost', 8080, app)

然后我们可以向它发送一个包含多行标头的请求:

GET / HTTP/1.1
Host: localhost:8080
Connection: close
X-Example: foo
 bar

预期的服务器输出:
标题值合并为一行

'foo bar'

实际服务器输出(Python 2):

----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 57361)
Traceback (most recent call last):
  File "/usr/lib/python2.7/SocketServer.py", line 295, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 321, in process_request
    self.finish_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 334, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "/usr/lib/python2.7/SocketServer.py", line 649, in __init__
    self.handle()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 320, in handle
    rv = BaseHTTPRequestHandler.handle(self)
  File "/usr/lib/python2.7/BaseHTTPServer.py", line 340, in handle
    self.handle_one_request()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 355, in handle_one_request
    return self.run_wsgi()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 238, in run_wsgi
    self.environ = environ = self.make_environ()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 217, in make_environ
    for key, value in self.get_header_items():
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 441, in get_header_items
    key, value = header[0:-2].split(":", 1)
ValueError: need more than 1 value to unpack
----------------------------------------

实际服务器输出(Python 3):
标头值包含 WSGI 规范不允许的换行符

'foo\r\n bar'

第二个错误与Headers对象如何处理换行符有关。

>>> from werkzeug import Headers
>>> h1 = Headers([('X-Example', 'foo\r\n bar')])
>>> h2 = Headers()
>>> h2.add('X-Example', 'foo\r\n bar')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1166, in add
    self._validate_value(_value)
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1173, in _validate_value
    raise ValueError('Detected newline in header value.  This is '
ValueError: Detected newline in header value.  This is a potential security problem

预期结果:
无论是ValueError还是成功,这两个操作都应该具有相同的结果。

实际结果:
Headers构造函数允许换行,而Headers.add()会引发ValueError

所有8条评论

HTTP头不允许换行。 您引用的部分讨论了折叠,它应该从展开的值中删除换行符。 这:

"""foo
     bar"""

应该展开成这样:

"foo bar"

除此之外,Werkzeug 不在此级别解析 HTTP,这是 WSGI 服务器的工作。 它在解析请求时拒绝换行的唯一原因是为了捕捉安全问题。

这张票的动机是 Flask 在开发模式下在 nginx 代理转发客户端证书背后的真实行为。 通过该设置,我观察到传递给应用程序的标头中的换行符。 但是当我试图在单元测试中复制它时,我在构建请求标头时得到了上面的ValueError

我对这个问题做了更多的研究,发现以下内容:

  • HTTP 规范 ( RFC 2616 ) 指出,标头中的换行符可以替换为单个空格,而不是保证它们是(参见上面的引用)。

  • WSGI 扩展的 CGI 规范 ( RFC 3875 ) 要求替换请求标头中的换行符:

    类似地,跨越多行的标题字段必须合并为一行。

  • WSGI 规范 ( PEP 333 ) 也禁止在响应头中使用换行符:

    每个header_value不得包含任何控制字符,包括回车符或换行符,无论是嵌入的还是末尾的。

所以这意味着这里有两个错误:

  • werkzeug 开发服务器未正确规范化请求标头中的折叠字符串。

  • Headers对象在接受标头值中的换行符方面不一致。 构造函数中接受换行符,但add()方法中不接受换行符。 在BaseResponse实际构造 WSGI 响应时, Headers对象保持宽松并执行验证可能更好。 这样在构造函数中禁止换行不会破坏与可能不符合标准的 WSGI 服务器(例如 werkzeug 自己的开发服务器)的兼容性。

很公平。 还有#1070 也参与其中。

很确定这已通过 #1070 的修复程序修复。 如果没有,请告诉我一个可重现的例子。

@davidism正如我在之前的评论中提到的,这里实际上有两个错误,当前的主分支上都没有修复。


第一个错误涉及 werkzeug 开发服务器如何处理换行的标头。 可以使用以下服务器代码重现它,该代码打印X-Example标头的值:

from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response

def app(environ, start_response):
    request = Request(environ)
    print(repr(request.headers.get('X-Example')))
    response = Response(status=204)
    return response(environ, start_response)

run_simple('localhost', 8080, app)

然后我们可以向它发送一个包含多行标头的请求:

GET / HTTP/1.1
Host: localhost:8080
Connection: close
X-Example: foo
 bar

预期的服务器输出:
标题值合并为一行

'foo bar'

实际服务器输出(Python 2):

----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 57361)
Traceback (most recent call last):
  File "/usr/lib/python2.7/SocketServer.py", line 295, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 321, in process_request
    self.finish_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 334, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "/usr/lib/python2.7/SocketServer.py", line 649, in __init__
    self.handle()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 320, in handle
    rv = BaseHTTPRequestHandler.handle(self)
  File "/usr/lib/python2.7/BaseHTTPServer.py", line 340, in handle
    self.handle_one_request()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 355, in handle_one_request
    return self.run_wsgi()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 238, in run_wsgi
    self.environ = environ = self.make_environ()
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 217, in make_environ
    for key, value in self.get_header_items():
  File "/home/.../venv/local/lib/python2.7/site-packages/werkzeug/serving.py", line 441, in get_header_items
    key, value = header[0:-2].split(":", 1)
ValueError: need more than 1 value to unpack
----------------------------------------

实际服务器输出(Python 3):
标头值包含 WSGI 规范不允许的换行符

'foo\r\n bar'

第二个错误与Headers对象如何处理换行符有关。

>>> from werkzeug import Headers
>>> h1 = Headers([('X-Example', 'foo\r\n bar')])
>>> h2 = Headers()
>>> h2.add('X-Example', 'foo\r\n bar')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1166, in add
    self._validate_value(_value)
  File "/home/.../venv3/lib/python3.6/site-packages/werkzeug/datastructures.py", line 1173, in _validate_value
    raise ValueError('Detected newline in header value.  This is '
ValueError: Detected newline in header value.  This is a potential security problem

预期结果:
无论是ValueError还是成功,这两个操作都应该具有相同的结果。

实际结果:
Headers构造函数允许换行,而Headers.add()会引发ValueError

我最近在我们的一个项目(带有 Flask 的 Python 2.7)中看到了ValueError ,它恰好位于 nginx 代理后面。 我最终恢复到0.14.1 (覆盖与Flask捆绑在一起的版本)并且我的错误消失了。 我想我会添加,因为似乎0.15.x分支引入了这个问题(或者可能会在处理请求标头方面产生一个新问题)。

- - - 更新 - - - -:

我追踪了我们传递的头文件,其中之一是 pem 格式的多行证书,即:

SSL_CLIENT_CERT: -----BEGIN CERTIFICATE-----
    MIIFHzCCAwegAwIBAgICEDgwDQYJKoZIhvcNAQELBQAwajELMAkGA1UEBhMCdXMx
    GDAWBgNVBAoMD3Uucy4gZ292ZXJubWVudDEPMA0GA1UECwwGcGVvcGxlMQwwCgYD
    VQQLDANkYWUxEDAOBgNVBAsMB2NoaW1lcmExEDAOBgNVBAMMB0ludGVyQ0EwHhcN
    MTcwODMxMTUwMzEwWhcNMjcwODI5MTUwMzEwWjBwMQswCQYDVQQGEwJVUzEYMBYG
    A1UECgwPVS5TLiBHb3Zlcm5tZW50MRAwDgYDVQQLDAdjaGltZXJhMQwwCgYDVQQL
    ....  
    -----END CERTIFICATE-----

我们的 nginx 服务器配置如下:

proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;

我们可能应该使用$ssl_client_escaped_cert而不是$ssl_client_cert (因为无论如何都不推荐使用)。 不确定该更改是否会解决标头解析问题。

希望这可以帮助遇到此问题的任何其他人。 似乎0.15.1目前无法使用 Python 2.7 正确处理多行标题,如 PEM 证书。

这又是2.7的问题,是开发服务器的header处理引起的。 我正在为请求标头的 2.7 兼容性代码添加处理标头折叠的功能。

确认开发服务器错误现在在 Python 2 和 3 中都已修复。 Headers对象的第二个问题仍然存在。 打开#1608。

此页面是否有帮助?
0 / 5 - 0 等级