Werkzeug: Werkzeug lida incorretamente com cabeçalhos de várias linhas

Criado em 10 mar. 2017  ·  8Comentários  ·  Fonte: pallets/werkzeug

De acordo com RFC 2616 :

Os valores do campo de cabeçalho HTTP / 1.1 podem ser dobrados em várias linhas se a linha de continuação começar com um espaço ou tabulação horizontal. Todo espaço em branco linear, incluindo dobradura, tem a mesma semântica de SP. Um destinatário PODE substituir qualquer espaço em branco linear por um único SP antes de interpretar o valor do campo ou encaminhar a mensagem a jusante.

No entanto, werkzeug não aceita valores de cabeçalho com novas linhas, mesmo se eles obedecerem a esta convenção.

>>> 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

Além disso, essa restrição é aplicada de forma inconsistente.

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

Encontrei esse problema ao tentar escrever casos de teste relacionados ao encaminhamento do nginx de certificados de cliente por meio de cabeçalhos, portanto, há um caso de uso real para oferecer suporte adequado.

bug server

Comentários muito úteis

@davidism Como mencionei em um comentário anterior, há na verdade dois bugs aqui, nenhum dos quais foi corrigido no branch master atual.


O primeiro bug envolve como o servidor de desenvolvimento werkzeug lida com cabeçalhos quebrados por linha. Ele pode ser reproduzido com o seguinte código de servidor, que imprime o valor do cabeçalho 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)

Podemos então enviar uma solicitação com um cabeçalho que abrange várias linhas:

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

Saída esperada do servidor:
O valor do cabeçalho é mesclado em uma única linha

'foo bar'

Saída real do servidor (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
----------------------------------------

Saída real do servidor (Python 3):
O valor do cabeçalho contém caracteres de nova linha não permitidos pela especificação WSGI

'foo\r\n bar'

O segundo bug tem a ver com como o objeto Headers lida com valores de nova linha.

>>> 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

Resultado esperado:
Ambas as operações devem ter o mesmo resultado, seja ValueError ou sucesso.

Resultado atual:
Headers construtor Headers.add() aumenta ValueError .

Todos 8 comentários

Os cabeçalhos HTTP não permitem novas linhas. A seção que você está citando fala sobre dobrar, que deve remover novas linhas do valor desdobrado. Esta:

"""foo
     bar"""

deve ser desdobrado para algo assim:

"foo bar"

Além disso, Werkzeug não analisa HTTP neste nível, este é o trabalho do servidor WSGI. A única razão pela qual ele rejeita novas linhas ao analisar solicitações é para detectar problemas de segurança.

Esse tíquete foi motivado pelo comportamento real do Flask no modo de desenvolvimento por trás de um proxy nginx que encaminha certificados de cliente. Com essa configuração, observei novas linhas nos cabeçalhos sendo passados ​​para o aplicativo. Mas quando tentei replicar isso em um teste de unidade, obtive o ValueError ao construir os cabeçalhos de solicitação.

Pesquisei um pouco mais sobre esse problema e descobri o seguinte:

  • A especificação HTTP ( RFC 2616 ) afirma que as novas linhas nos cabeçalhos PODEM ser substituídas por um único espaço, não que seja garantido (veja a citação acima).

  • A especificação CGI ( RFC 3875 ), que WSGI estende, exige que as novas linhas nos cabeçalhos de solicitação sejam substituídas:

    Da mesma forma, um campo de cabeçalho que abrange várias linhas DEVE ser mesclado em uma única linha.

  • A especificação WSGI ( PEP 333 ) também proíbe novas linhas nos cabeçalhos de resposta:

    Cada header_value não deve incluir nenhum caractere de controle, incluindo retornos de carro ou avanços de linha, incorporados ou no final.

Então, isso significa que existem dois bugs aqui:

  • O servidor de desenvolvimento werkzeug não normaliza corretamente as strings dobradas nos cabeçalhos de solicitação.

  • O objeto Headers é inconsistente quanto à aceitação de novas linhas nos valores do cabeçalho. Novas linhas são aceitas no construtor, mas não no método add() . Provavelmente é melhor para o objeto Headers permanecer permissivo e executar a validação ao construir a resposta WSGI em BaseResponse . Dessa forma, proibir novas linhas no construtor não quebrará a compatibilidade com servidores WSGI possivelmente não conformes (como o servidor de desenvolvimento do próprio werkzeug).

Justo. Há também o # 1070 que ajuda nisso.

Quase certo que isso foi corrigido com a correção para # 1070. Caso contrário, informe-nos com um exemplo reproduzível.

@davidism Como mencionei em um comentário anterior, há na verdade dois bugs aqui, nenhum dos quais foi corrigido no branch master atual.


O primeiro bug envolve como o servidor de desenvolvimento werkzeug lida com cabeçalhos quebrados por linha. Ele pode ser reproduzido com o seguinte código de servidor, que imprime o valor do cabeçalho 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)

Podemos então enviar uma solicitação com um cabeçalho que abrange várias linhas:

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

Saída esperada do servidor:
O valor do cabeçalho é mesclado em uma única linha

'foo bar'

Saída real do servidor (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
----------------------------------------

Saída real do servidor (Python 3):
O valor do cabeçalho contém caracteres de nova linha não permitidos pela especificação WSGI

'foo\r\n bar'

O segundo bug tem a ver com como o objeto Headers lida com valores de nova linha.

>>> 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

Resultado esperado:
Ambas as operações devem ter o mesmo resultado, seja ValueError ou sucesso.

Resultado atual:
Headers construtor Headers.add() aumenta ValueError .

Eu estava vendo o ValueError recentemente em um de nossos projetos (Python 2.7 com Flask) que está atrás de um proxy nginx. Acabei revertendo para 0.14.1 (substituindo a versão empacotada com Flask ) e meu erro foi embora. Pensei em adicionar, pois parece que o branch 0.15.x introduziu esse problema (ou potencialmente criou um novo problema com a forma como estava lidando com cabeçalhos de solicitação).

------ATUALIZAR-------:

Eu rastreei quais cabeçalhos estávamos passando e um deles era um certificado multilinha em formato pem, ou seja:

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

Nosso servidor nginx foi configurado assim:

proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;

Provavelmente deveríamos usar $ssl_client_escaped_cert vez de $ssl_client_cert (já que está obsoleto de qualquer maneira). Não tenho certeza se essa mudança vai resolver o problema de análise do cabeçalho.

Esperando que isso ajude qualquer outra pessoa que esteja enfrentando esse problema. Parece que 0.15.1 não lida corretamente com um cabeçalho de várias linhas como um certificado PEM neste momento com Python 2.7.

Este é um problema 2.7 causado novamente pelo processamento do cabeçalho do servidor de desenvolvimento. Estou adicionando a capacidade de processar a dobradura de cabeçalhos ao código de compatibilidade 2.7 para cabeçalhos de solicitação.

Confirmado que o bug do servidor de desenvolvimento foi corrigido no Python 2 e 3. O segundo problema com o objeto Headers ainda está presente. Aberto no. 1608.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

abathur picture abathur  ·  13Comentários

davidism picture davidism  ·  9Comentários

d42 picture d42  ·  6Comentários

Nessphoro picture Nessphoro  ·  6Comentários

caiz picture caiz  ·  3Comentários