Werkzeug: Werkzeug gère de manière incorrecte les en-têtes multilignes

Créé le 10 mars 2017  ·  8Commentaires  ·  Source: pallets/werkzeug

Selon RFC 2616 :

Les valeurs des champs d'en-tête HTTP/1.1 peuvent être repliées sur plusieurs lignes si la ligne de continuation commence par un espace ou une tabulation horizontale. Tous les espaces blancs linéaires, y compris le pliage, ont la même sémantique que SP. Un destinataire PEUT remplacer tout espace blanc linéaire par un seul SP avant d'interpréter la valeur du champ ou de transmettre le message en aval.

Cependant, werkzeug n'accepte pas les valeurs d'en-tête avec des sauts de ligne, même si elles respectent cette convention.

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

De plus, cette restriction est appliquée de manière incohérente.

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

J'ai rencontré ce problème en essayant d'écrire des cas de test concernant le transfert nginx de certificats client via des en-têtes, il existe donc un cas d'utilisation réel pour le prendre en charge correctement.

bug server

Commentaire le plus utile

@davidism Comme je l'ai mentionné dans un commentaire précédent, il y a en fait deux bogues ici, dont aucun n'a été corrigé sur la branche master actuelle.


Le premier bogue concerne la façon dont le serveur de développement werkzeug gère les en-têtes enroulés en ligne. Il peut être reproduit avec le code serveur suivant, qui imprime la valeur de l'en-tête 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)

Nous pouvons ensuite lui envoyer une requête avec un en-tête sur plusieurs lignes :

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

Sortie serveur attendue :
La valeur de l'en-tête est fusionnée sur une seule ligne

'foo bar'

Sortie réelle du serveur (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
----------------------------------------

Sortie réelle du serveur (Python 3) :
La valeur d'en-tête contient des caractères de nouvelle ligne non autorisés par la spécification WSGI

'foo\r\n bar'

Le deuxième bogue concerne la façon dont l'objet Headers gère les valeurs de nouvelle ligne.

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

Résultat attendu:
Les deux opérations doivent avoir le même résultat, qu'il s'agisse d'un ValueError ou d'un succès.

Résultat actuel:
Headers autorise les nouvelles lignes tandis que Headers.add() augmente ValueError .

Tous les 8 commentaires

Les en-têtes HTTP n'autorisent pas les nouvelles lignes. La section que vous citez parle de pliage, ce qui devrait supprimer les nouvelles lignes de la valeur dépliée. Cette:

"""foo
     bar"""

devrait se dérouler à quelque chose comme ceci:

"foo bar"

En dehors de cela, Werkzeug n'analyse pas HTTP à ce niveau, c'est le travail du serveur WSGI. La seule raison pour laquelle il rejette les nouvelles lignes lors de l'analyse des requêtes est de détecter les problèmes de sécurité.

Ce ticket a été motivé par le comportement réel de Flask en mode développement derrière un proxy nginx transférant des certificats client. Avec cette configuration, j'ai observé des nouvelles lignes dans les en-têtes transmises à l'application. Mais lorsque j'ai tenté de reproduire cela dans un test unitaire, j'ai obtenu les ValueError ci-dessus lors de la création des en-têtes de requête.

J'ai fait un peu plus de recherches sur ce problème et j'ai trouvé ce qui suit :

  • La spécification HTTP ( RFC 2616 ) indique que les nouvelles lignes dans les en-têtes PEUVENT être remplacées par un seul espace, pas qu'elles soient garanties (voir la citation ci-dessus).

  • La spécification CGI ( RFC 3875 ), que WSGI étend, nécessite le remplacement des nouvelles lignes dans les en-têtes de requête :

    De même, un champ d'en-tête qui s'étend sur plusieurs lignes DOIT être fusionné sur une seule ligne.

  • La spécification WSGI ( PEP 333 ) interdit également les nouvelles lignes dans les en-têtes de réponse :

    Chaque header_value ne doit inclure aucun caractère de contrôle, y compris les retours chariot ou les sauts de ligne, qu'ils soient intégrés ou à la fin.

Cela signifie donc qu'il y a deux bugs ici :

  • Le serveur de développement werkzeug ne normalise pas correctement les chaînes pliées dans les en-têtes de requête.

  • L'objet Headers n'accepte pas les nouvelles lignes dans les valeurs d'en-tête. Les sauts de ligne sont acceptés dans le constructeur, mais pas dans la méthode add() . Il est probablement préférable que l'objet Headers reste permissif et effectue la validation lors de la construction de la réponse WSGI dans BaseResponse . De cette façon, interdire les nouvelles lignes dans le constructeur ne rompra pas la compatibilité avec les serveurs WSGI éventuellement non conformes (tels que le propre serveur de développement de werkzeug).

Assez juste. Il y a aussi #1070 qui joue là-dedans.

Assez sûr que cela a été corrigé avec le correctif pour #1070. Si ce n'est pas le cas, faites-le moi savoir avec un exemple reproductible.

@davidism Comme je l'ai mentionné dans un commentaire précédent, il y a en fait deux bogues ici, dont aucun n'a été corrigé sur la branche master actuelle.


Le premier bogue concerne la façon dont le serveur de développement werkzeug gère les en-têtes enroulés en ligne. Il peut être reproduit avec le code serveur suivant, qui imprime la valeur de l'en-tête 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)

Nous pouvons ensuite lui envoyer une requête avec un en-tête sur plusieurs lignes :

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

Sortie serveur attendue :
La valeur de l'en-tête est fusionnée sur une seule ligne

'foo bar'

Sortie réelle du serveur (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
----------------------------------------

Sortie réelle du serveur (Python 3) :
La valeur d'en-tête contient des caractères de nouvelle ligne non autorisés par la spécification WSGI

'foo\r\n bar'

Le deuxième bogue concerne la façon dont l'objet Headers gère les valeurs de nouvelle ligne.

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

Résultat attendu:
Les deux opérations doivent avoir le même résultat, qu'il s'agisse d'un ValueError ou d'un succès.

Résultat actuel:
Headers autorise les nouvelles lignes tandis que Headers.add() augmente ValueError .

Je voyais le ValueError récemment dans l'un de nos projets (Python 2.7 avec Flask) qui se trouve derrière un proxy nginx. J'ai fini par revenir à 0.14.1 (en remplaçant la version fournie avec Flask ) et mon erreur a disparu. J'ai pensé ajouter car il semble que la branche 0.15.x introduit ce problème (ou ait potentiellement créé un nouveau problème avec la façon dont elle gérait les en-têtes de demande).

------METTRE À JOUR-------:

J'ai recherché les en-têtes que nous passions et l'un d'eux était un certificat multiligne au format pem, c'est-à-dire :

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

Notre serveur nginx a été configuré comme suit :

proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;

Nous devrions probablement utiliser $ssl_client_escaped_cert au lieu de $ssl_client_cert (puisque c'est de toute façon obsolète). Je ne sais pas si ce changement résoudra le problème d'analyse d'en-tête.

En espérant que cela aide quelqu'un d'autre qui rencontre ce problème. Il semble que 0.15.1 ne gère pas correctement un en-tête multiligne comme un certificat PEM pour le moment avec Python 2.7.

Il s'agit à nouveau d'un problème 2.7 causé par le traitement des en-têtes du serveur de développement. J'ajoute la possibilité de traiter le pliage des en-têtes au code de compatibilité 2.7 pour les en-têtes de demande.

Confirmé que le bogue du serveur de développement est maintenant corrigé dans Python 2 et 3. Le deuxième problème avec l'objet Headers est toujours présent. Ouvert #1608.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

androiddrew picture androiddrew  ·  14Commentaires

mrx23dot picture mrx23dot  ·  6Commentaires

miki725 picture miki725  ·  10Commentaires

asottile picture asottile  ·  11Commentaires

masklinn picture masklinn  ·  11Commentaires