وفقًا لـ 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 لشهادات العميل عبر الرؤوس ، لذلك هناك حالة استخدام حقيقية لدعم هذا بشكل صحيح.
لا تسمح رؤوس 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.
التعليق الأكثر فائدة
davidism كما ذكرت في تعليق سابق ، يوجد في الواقع
يتضمن الخطأ الأول كيفية تعامل خادم تطوير werkzeug مع الرؤوس المغلفة بالسطر. يمكن إعادة إنتاجه باستخدام كود الخادم التالي ، والذي يطبع قيمة رأس
X-Example
:يمكننا بعد ذلك إرسال طلب برأس يمتد على عدة أسطر:
مخرجات الخادم المتوقعة:
يتم دمج قيمة الرأس في سطر واحد
مخرجات الخادم الفعلية (Python 2):
مخرجات الخادم الفعلية (Python 3):
تحتوي قيمة الرأس على أحرف سطر جديد غير مسموح بها بواسطة مواصفات WSGI
يتعلق الخطأ الثاني بكيفية تعامل الكائن
Headers
مع قيم السطر الجديد.نتيجة متوقعة:
يجب أن يكون لكلتا العمليتين نفس النتيجة ، سواء كانت
ValueError
أو نجاح.نتيجة فعلية:
يسمح المُنشئ
Headers
بالخطوط الجديدة بينما يقومHeaders.add()
برفعValueError
.