Werkzeug: يعالج Werkzeug الرؤوس متعددة الأسطر بشكل غير صحيح

تم إنشاؤها على ١٠ مارس ٢٠١٧  ·  8تعليقات  ·  مصدر: pallets/werkzeug

وفقًا لـ RFC 2616 :

يمكن طي قيم حقل رأس HTTP / 1.1 على عدة أسطر إذا بدأ سطر المتابعة بمسافة أو علامة تبويب أفقية. تحتوي جميع المسافات البيضاء الخطية ، بما في ذلك الطي ، على نفس دلالات 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 ) على أنه يمكن استبدال الأسطر الجديدة في الرؤوس بمسافة واحدة ، وليس مضمونًا (انظر الاقتباس أعلاه).

  • تتطلب مواصفات CGI ( RFC 3875 ) ، التي تمدها WSGI ، استبدال الأسطر الجديدة في رؤوس الطلبات:

    وبالمثل ، يجب دمج حقل الرأس الذي يمتد على عدة أسطر في سطر واحد.

  • تحظر مواصفات WSGI ( PEP 333 ) أيضًا الأسطر الجديدة في رؤوس الاستجابة:

    يجب ألا يتضمن كل header_value أي أحرف تحكم ، بما في ذلك أحرف الإرجاع أو موجز السطر ، سواء أكان مضمّنًا أو في النهاية.

هذا يعني أن هناك خطأين هنا:

  • لا يقوم خادم تطوير werkzeug بتسوية السلاسل المطوية بشكل صحيح في رؤوس الطلبات.

  • الكائن Headers غير متسق حول قبول الأسطر الجديدة في قيم الرأس. يتم قبول الأسطر الجديدة في المنشئ ، ولكن ليس في طريقة add() . من الأفضل أن يظل الكائن Headers متسامحًا وأن يقوم بالتحقق من الصحة عند إنشاء استجابة WSGI في BaseResponse . بهذه الطريقة ، لن يؤدي منع الأسطر الجديدة في المُنشئ إلى كسر التوافق مع خوادم 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 .

كنت أرى ValueError مؤخرًا في أحد مشاريعنا (Python 2.7 مع Flask) والذي يحدث خلف وكيل 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 لا يتعامل بشكل صحيح مع رأس متعدد الأسطر مثل شهادة PEM في هذا الوقت مع Python 2.7.

هذه مشكلة 2.7 مرة أخرى بسبب معالجة رأس خادم التطوير. أقوم بإضافة القدرة على معالجة طي الرؤوس إلى كود التوافق 2.7 لرؤوس الطلبات.

تم التأكيد على أن خطأ خادم التطوير قد تم إصلاحه الآن في كل من Python 2 و 3. المشكلة الثانية مع الكائن Headers لا تزال موجودة. افتتح # 1608.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

abathur picture abathur  ·  13تعليقات

Nessphoro picture Nessphoro  ·  6تعليقات

masklinn picture masklinn  ·  11تعليقات

miki725 picture miki725  ·  10تعليقات

davidism picture davidism  ·  9تعليقات