From f9244201cb1d3802e61549329b7c48175d8a71d9 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 18 Apr 2023 16:15:31 -0700 Subject: [PATCH] Backport fix for CVE-2023-25577 Also bring over tests from dist-git, and add test for formparser to test the backported changes. Resolves: rhbz#2188442 --- ...kport-limits-for-multiple-form-parts.patch | 143 ++++++ python-werkzeug.spec | 20 +- .../multipart/firefox3-2png1txt/file1.png | Bin 0 -> 523 bytes .../multipart/firefox3-2png1txt/file2.png | Bin 0 -> 703 bytes .../multipart/firefox3-2png1txt/request.txt | Bin 0 -> 1739 bytes .../multipart/firefox3-2png1txt/text.txt | 1 + .../multipart/firefox3-2pnglongtext/file1.png | Bin 0 -> 781 bytes .../multipart/firefox3-2pnglongtext/file2.png | Bin 0 -> 733 bytes .../firefox3-2pnglongtext/request.txt | Bin 0 -> 2042 bytes .../multipart/firefox3-2pnglongtext/text.txt | 3 + .../scripts/multipart/ie6-2png1txt/file1.png | Bin 0 -> 523 bytes .../scripts/multipart/ie6-2png1txt/file2.png | Bin 0 -> 703 bytes .../multipart/ie6-2png1txt/request.txt | Bin 0 -> 1798 bytes tests/scripts/multipart/ie6-2png1txt/text.txt | 1 + .../multipart/ie7_full_path_request.txt | Bin 0 -> 30044 bytes .../multipart/opera8-2png1txt/file1.png | Bin 0 -> 582 bytes .../multipart/opera8-2png1txt/file2.png | Bin 0 -> 733 bytes .../multipart/opera8-2png1txt/request.txt | Bin 0 -> 1740 bytes .../multipart/opera8-2png1txt/text.txt | 1 + tests/scripts/multipart/test_collect.py | 56 +++ .../multipart/webkit3-2png1txt/file1.png | Bin 0 -> 1002 bytes .../multipart/webkit3-2png1txt/file2.png | Bin 0 -> 952 bytes .../multipart/webkit3-2png1txt/request.txt | Bin 0 -> 2408 bytes .../multipart/webkit3-2png1txt/text.txt | 1 + tests/scripts/res/test.txt | 1 + tests/scripts/run_tests.sh | 4 + tests/scripts/test_formparser.py | 468 +++++++++++++++++ tests/scripts/test_wsgi.py | 474 ++++++++++++++++++ tests/tests.yml | 15 + 29 files changed, 1182 insertions(+), 6 deletions(-) create mode 100644 0001-Backport-limits-for-multiple-form-parts.patch create mode 100644 tests/scripts/multipart/firefox3-2png1txt/file1.png create mode 100644 tests/scripts/multipart/firefox3-2png1txt/file2.png create mode 100644 tests/scripts/multipart/firefox3-2png1txt/request.txt create mode 100644 tests/scripts/multipart/firefox3-2png1txt/text.txt create mode 100644 tests/scripts/multipart/firefox3-2pnglongtext/file1.png create mode 100644 tests/scripts/multipart/firefox3-2pnglongtext/file2.png create mode 100644 tests/scripts/multipart/firefox3-2pnglongtext/request.txt create mode 100644 tests/scripts/multipart/firefox3-2pnglongtext/text.txt create mode 100644 tests/scripts/multipart/ie6-2png1txt/file1.png create mode 100644 tests/scripts/multipart/ie6-2png1txt/file2.png create mode 100644 tests/scripts/multipart/ie6-2png1txt/request.txt create mode 100644 tests/scripts/multipart/ie6-2png1txt/text.txt create mode 100644 tests/scripts/multipart/ie7_full_path_request.txt create mode 100644 tests/scripts/multipart/opera8-2png1txt/file1.png create mode 100644 tests/scripts/multipart/opera8-2png1txt/file2.png create mode 100644 tests/scripts/multipart/opera8-2png1txt/request.txt create mode 100644 tests/scripts/multipart/opera8-2png1txt/text.txt create mode 100644 tests/scripts/multipart/test_collect.py create mode 100644 tests/scripts/multipart/webkit3-2png1txt/file1.png create mode 100644 tests/scripts/multipart/webkit3-2png1txt/file2.png create mode 100644 tests/scripts/multipart/webkit3-2png1txt/request.txt create mode 100644 tests/scripts/multipart/webkit3-2png1txt/text.txt create mode 100644 tests/scripts/res/test.txt create mode 100755 tests/scripts/run_tests.sh create mode 100644 tests/scripts/test_formparser.py create mode 100644 tests/scripts/test_wsgi.py create mode 100644 tests/tests.yml diff --git a/0001-Backport-limits-for-multiple-form-parts.patch b/0001-Backport-limits-for-multiple-form-parts.patch new file mode 100644 index 0000000..2fe8cff --- /dev/null +++ b/0001-Backport-limits-for-multiple-form-parts.patch @@ -0,0 +1,143 @@ +From 722f58f8221c013e2dd6cf1fde59fd686619f483 Mon Sep 17 00:00:00 2001 +From: "Brian C. Lane" +Date: Tue, 18 Apr 2023 15:57:50 -0700 +Subject: [PATCH] Backport limits for multiple form parts + +This fixes CVE-2023-25577, and is backported from the fix for 2.2.3 +here: +https://github.com/pallets/werkzeug/commit/517cac5a804e8c4dc4ed038bb20dacd038e7a9f1 + +It adds a max_form_parts limit that defaults to 1000. This will prevent +memory exhaustion when it receives too many form parts. + +Resolves: rhbz#2170325 +--- + tests/test_formparser.py | 9 +++++++++ + werkzeug/formparser.py | 15 ++++++++++++--- + werkzeug/wrappers.py | 12 +++++++++++- + 3 files changed, 32 insertions(+), 4 deletions(-) + +diff --git a/tests/test_formparser.py b/tests/test_formparser.py +index c140476b..23f44800 100644 +--- a/tests/test_formparser.py ++++ b/tests/test_formparser.py +@@ -101,6 +101,15 @@ class TestFormParser(object): + req.max_form_memory_size = 400 + strict_eq(req.form['foo'], u'Hello World') + ++ # Test fix for CVE-2023-25577 that limits on form-data raise an error ++ req = Request.from_values(input_stream=BytesIO(data), ++ content_length=len(data), ++ content_type='multipart/form-data; boundary=foo', ++ method='POST') ++ req.max_form_parts = 1 ++ pytest.raises(RequestEntityTooLarge, lambda: req.form['foo']) ++ ++ + def test_missing_multipart_boundary(self): + data = (b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n' + b'Hello World\r\n' +diff --git a/werkzeug/formparser.py b/werkzeug/formparser.py +index a0118054..7a21e123 100644 +--- a/werkzeug/formparser.py ++++ b/werkzeug/formparser.py +@@ -137,12 +137,14 @@ class FormDataParser(object): + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. ++ :param max_form_parts: The maximum number of parts to be parsed. If this is ++ exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised. + """ + + def __init__(self, stream_factory=None, charset='utf-8', + errors='replace', max_form_memory_size=None, + max_content_length=None, cls=None, +- silent=True): ++ silent=True, max_form_parts=None): + if stream_factory is None: + stream_factory = default_stream_factory + self.stream_factory = stream_factory +@@ -154,6 +156,7 @@ class FormDataParser(object): + cls = MultiDict + self.cls = cls + self.silent = silent ++ self.max_form_parts = max_form_parts + + def get_parse_func(self, mimetype, options): + return self.parse_functions.get(mimetype) +@@ -203,7 +206,7 @@ class FormDataParser(object): + def _parse_multipart(self, stream, mimetype, content_length, options): + parser = MultiPartParser(self.stream_factory, self.charset, self.errors, + max_form_memory_size=self.max_form_memory_size, +- cls=self.cls) ++ cls=self.cls, max_form_parts=self.max_form_parts) + boundary = options.get('boundary') + if boundary is None: + raise ValueError('Missing boundary') +@@ -285,12 +288,14 @@ _end = 'end' + class MultiPartParser(object): + + def __init__(self, stream_factory=None, charset='utf-8', errors='replace', +- max_form_memory_size=None, cls=None, buffer_size=64 * 1024): ++ max_form_memory_size=None, cls=None, buffer_size=64 * 1024, ++ max_form_parts=None): + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + self.stream_factory = default_stream_factory if stream_factory is None else stream_factory + self.cls = MultiDict if cls is None else cls ++ self.max_form_parts = max_form_parts + + # make sure the buffer size is divisible by four so that we can base64 + # decode chunk by chunk +@@ -472,6 +477,7 @@ class MultiPartParser(object): + ``('form', (name, val))`` parts. + """ + in_memory = 0 ++ parts_decoded = 0 + + for ellt, ell in self.parse_lines(file, boundary, content_length): + if ellt == _begin_file: +@@ -500,6 +506,9 @@ class MultiPartParser(object): + self.in_memory_threshold_reached(in_memory) + + elif ellt == _end: ++ parts_decoded += 1 ++ if self.max_form_parts is not None and parts_decoded > self.max_form_parts: ++ raise exceptions.RequestEntityTooLarge() + if is_file: + container.seek(0) + yield ('file', +diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py +index ea35228e..eca7eec7 100644 +--- a/werkzeug/wrappers.py ++++ b/werkzeug/wrappers.py +@@ -169,6 +169,15 @@ class BaseRequest(object): + #: .. versionadded:: 0.5 + max_form_memory_size = None + ++ #: The maximum number of multipart parts to parse, passed to ++ #: :attr:`form_data_parser_class`. Parsing form data with more than this ++ #: many parts will raise :exc:`~.RequestEntityTooLarge`. ++ #: ++ #: Backported to 0.12.2 from 2.2.3 ++ #: ++ #: .. versionadded:: 2.2.3 ++ max_form_parts = 1000 ++ + #: the class to use for `args` and `form`. The default is an + #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports + #: multiple values per key. alternatively it makes sense to use an +@@ -345,7 +354,8 @@ class BaseRequest(object): + self.encoding_errors, + self.max_form_memory_size, + self.max_content_length, +- self.parameter_storage_class) ++ self.parameter_storage_class, ++ max_form_parts=self.max_form_parts) + + def _load_form_data(self): + """Method used internally to retrieve submitted data. After calling +-- +2.40.0 + diff --git a/python-werkzeug.spec b/python-werkzeug.spec index f69c994..b629aff 100644 --- a/python-werkzeug.spec +++ b/python-werkzeug.spec @@ -9,7 +9,7 @@ Name: python-werkzeug Version: 0.12.2 -Release: 4%{?dist} +Release: 7%{?dist} Summary: The Swiss Army knife of Python web development Group: Development/Libraries @@ -20,6 +20,8 @@ Source0: https://files.pythonhosted.org/packages/source/W/Werkzeug/%{srcn # See https://github.com/mitsuhiko/werkzeug/issues/761 Source1: werkzeug-sphinx-theme.tar.gz +Patch0001: 0001-Backport-limits-for-multiple-form-parts.patch + BuildArch: noarch %global _description\ @@ -53,7 +55,7 @@ BuildRequires: python2-setuptools %{?python_provide:%python_provide python2-werkzeug} %description -n python2-werkzeug %_description -%endif # with python2 +%endif %package -n python3-werkzeug @@ -79,7 +81,7 @@ Documentation and examples for python-werkzeug. %prep -%setup -q -n %{srcname}-%{version} +%autosetup -p1 -n %{srcname}-%{version} %{__sed} -i 's/\r//' LICENSE %{__sed} -i '1d' tests/multipart/test_collect.py tar -xf %{SOURCE1} @@ -94,7 +96,7 @@ find %{py3dir} -name '*.py' | xargs sed -i '1s|^#!python|#!%{__python3}|' %py2_build find examples/ -name '*.py' -executable | xargs chmod -x find examples/ -name '*.png' -executable | xargs chmod -x -%endif # with python2 +%endif pushd %{py3dir} %py3_build @@ -113,7 +115,7 @@ popd %install %if %{with python2} %py2_install -%endif # with python2 +%endif pushd %{py3dir} %py3_install @@ -126,7 +128,7 @@ popd %license LICENSE %doc AUTHORS PKG-INFO CHANGES %{python2_sitelib}/* -%endif # with python2 +%endif %files -n python3-werkzeug %license LICENSE @@ -138,6 +140,12 @@ popd %changelog +* Mon Apr 24 2023 Brian C. Lane - 0.12.2-7 +- Add tests from dist-git +- Add new test for formdata +- Backport fix for CVE-2023-25577 + Resolves: rhbz#2188442 + * Fri Jun 22 2018 Charalampos Stratakis - 0.12.2-4 - Use python3-sphinx for the docs diff --git a/tests/scripts/multipart/firefox3-2png1txt/file1.png b/tests/scripts/multipart/firefox3-2png1txt/file1.png new file mode 100644 index 0000000000000000000000000000000000000000..9b3422c61e5d23434d085834b82eed7a7363976e GIT binary patch literal 523 zcmV+m0`&cfP)JNR5;6} zQ!#78Koq{K0R;zL3K^ueKcPdX4$`e#^8*A?$4(UrP73NEC=L?Wb`e}#+M#4AI28&O z(h7D{iBLo^6ibLnntoS;4W+RSeem(_-M#O-mv=AJwr%ns(1oktvOHk&-Wjo2=gN^Bj3SRR*}a0NpG5_ zolK`M<47b@MJ$6*<9VLqM#|QJ9FOl*`~9c!VzJ0#yuW&YL-uTJXs#7SsfEK~0YXX4 z0b#F1DNRn42?6;6#8Y4fXr9klsZFxeKF=OosmQ6jmTLP{}-6iF>nO8f(+aY2!Xm^*5Av6NhR zD0!Nar%`rnu~v@R4hO^8&iS41=VQN>+65QO>m_E!|B#IbuI^pAy5?9WYjHC`6;s8j z!_-hy%sJEya}KAD$C|Z-~!ZLFQctG09Uhp@QPcl?mU}7$E{?cz}t3fC%LK?;}5+keI!OTyHb6 z@j}nbBm-HHT&CJnb^IYBAVSCka_RdNzT74;XDve?FCx*eM2ky^TZSvCN4lf#zADBt{VLMZ58~8Xlk&tIj2}J+_M1=n24SuBB zC|kIW{HG=&F(WrHgY=^pRBSp=QTYN)m5{Hh{2@SBTQi04uPMk>dS9PrQdx|l%yhmz zOH#Sz0@1`YLTX0HPj&aS)SnFM)H&2CwbI1q`b)fRK1k<-HpW#}^Sv*{jiIgdH9W>t zQ6<#EFflVmJF;g{aA;S(kLP%K=NdiTT|X03N>|n%ZExo<#LO72ZdK{vlG)|{vIVoS lXs&IrKfQB(W4VA1FKr}=oF)ukIzeukjFI@>?NJwQt zs+B@!Zen_>K2V02iR|DNig)WpGT%PfAtr1Q{BXnwU~qcrw+7fq`+Zr;B5V#p$KN zhS`T4ME0#_WMz-#l3cRYexdVnzDsMPKQprg9`_UF^5c4?pus0}y-YAB`qsh}O$$*j zu}fTK!JPpv1|r@ap0n!0n|POQ;B;SW6OK48)px!e*CwdDUbEMMKMcST;-Q);oW!N*>dde zk?3_}W;uTN;)Ntb7x_&$-`u;!HKBs_g@@XzRqvvg9qlOBIkxXhHf!GN5-T(1WkH&+ zcC();iH*N~U}}Sk=W&lSjJ%ElQS0354&}BwB~FcAe?5B6WR12a#c!@jyYGq#=BFFY z+_<}`Xy=~Rzkxbp$}tVfpBC#R?cVy3<3n+&pU&&KQ|G$Nm?0Y)gdP z^a%$)H*n^!Qxo*epS5hR&vO089x6euI?Vx(E9X6OJ9Vu=rvH7&@4w4GCT%>Cv}b=+ zgY&nXoQ!p%t}9y)Co(#Do?%Q}@1p7JH-(e&C)1JO1h$NM=Ob2z@F|wD$XM84*8Q_o zqsx5Xvj_U$80YlZbv5_s-2uiXgQu&X%Q~loCSEQ)=~P#jdMVWiHKi65L&T zYD#9wz@=HHKv0^MkX(B3ZuZ$inFAlcKQFI)9&zrO!A-H5&sDB13RK!T%gM{IQ^Zp@ z(CZKLvO>p+?K7;i$~I1ksnDpLHEDTZS4y@Yb3MB< zBk+#4ev;0k{hQv26dXIx_AJ%dUUK!p12%0xpY^om7E7+a+iTlWW2`zw--5@$bWT&! z3MoI)aD$kG9Cr6rfB!L>_(<#9)Vq(b-u!D_bB3c+y+iUDr&`;J2bE17?H$g>*E_%2 zEnSpl-5J^a^P27aCu^5I-WTkCg4u_KMTt#oL6!>-*UQ^Sit;{@?Wj8z-Y_zcH z(aEWt_peAOGNn~CbT%|_Jo)`z*?!)H8GGAe@{KosbUJ@pg*kFU%(7d-rGFHa6r4^l z7rp!~vhR+XVY;rfyuQ-K^)B7s9no#tE1&Azy?*PZXj6lrQU`~k;6nY%xo_HMt31m~ z&?x28mzp3y!}C|7b#RvS4-1Ql6Vm-ST^Sf%TK+xZty^+SBYH>QpXF*whRWs!&0iLa zhvZoK2mWO8nL1(psy~Vj;nBve@7HUd2&synpB5C^v+zvGK3nggolluA9x-vsay#K) z`hCll`8=<#SYFZGdg;isU*3Ci?L3}0n;i=c`@C2GXz#+cUrcR|g#@ZzIb>jH>}I_w zJ+L4nd~g5ripu9wws&Lgg@e7qugT@+Ki5BUMq+JV=*tI_Za)3DiFNaajCHvi?DrPe wK3@I(&*A)Kt{Ug21wWiX_k328T2TTl`FXigD-v@Ha#9sQ0u<$NU0q%-0RLvCdjJ3c literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/firefox3-2png1txt/text.txt b/tests/scripts/multipart/firefox3-2png1txt/text.txt new file mode 100644 index 0000000..c87634d --- /dev/null +++ b/tests/scripts/multipart/firefox3-2png1txt/text.txt @@ -0,0 +1 @@ +example text \ No newline at end of file diff --git a/tests/scripts/multipart/firefox3-2pnglongtext/file1.png b/tests/scripts/multipart/firefox3-2pnglongtext/file1.png new file mode 100644 index 0000000000000000000000000000000000000000..89c8129a490b329f3165f32fa0781701aab417ea GIT binary patch literal 781 zcmV+o1M>WdP)4-QibtN)VXQDpczE`xXAkUjh%RI>;okxb7K@0kpyQ1k_Y(|Oe7$m(^ zNYX>mI||sUbmn+c3<&FnE=4u#()KBS^SH8e)Qs5i!#lY=$-1gbH6VluzU=m=EP78&5vQ z-?+fFP-G2l&l_QzYealK$;1Rl?FkzXR&Jv@fBPNjCr#AYRyJ7UJQ0v#?)7Ott=>3`#-pV!7>9}>Q1jL)H6h&gkP@3nI=+F3nA~M>u#(n* z8T!#8oEw&-mED4!h4s!N@Jo3S7N&Q6%6l3}nlcd~X@>;uelvPsSkXIgg~e+^T1zSf z3SNj(5%jK~i8@b;C9VHk(~TedF+gQSL8D5xnVSSWAVY>J9b+m>@{iq7_KE}go~11+5s4;8hc+i0Xa zI1j@EX5!S+Me6HNqKzU5YQwL;-W5$p%ZMKMeR<%zp69-~?<4?8|C8S?bklXr4v&Ov zb&06v2|-x?qB`90yn>Qi%Sh2^G4n)$ZdyvTPf9}1)_buUT7>`e2G&2VU@~Bb(o+Mz zi4)>IxlSY${Dj4k={-9RzU^W5g9|2V5RZ2ZulL9s2xQbZ@r6eP9Ra5u(s|C0Nj#&4>wTSkb?%#=9?@ z^oxDy-O@tyN{L@by(WWvQ3%CyEu8x{+#Jb4-h&K9Owi)2pgg+heWDyked|3R$$kL@A z#sp1v-r+=G4B8D6DqsDH0@7OztA7aT9qc1Py{()w`m``?Y0&gi2=ROcc-9+nU^I6< zT=e_Y=vSnG@?3Ue{BW5ONFttcE!R-R_W4O01|0-|K-YNXLo2`4Qv z`r1LxR6#yf3FB%T95gJnaKKivA~Z}S9A(ZxEDK}O3T04USJ P00000NkvXXu0mjf^IS-S literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/firefox3-2pnglongtext/request.txt b/tests/scripts/multipart/firefox3-2pnglongtext/request.txt new file mode 100644 index 0000000000000000000000000000000000000000..489290b6730d72648b7a748c5c2984aa7af3c2a5 GIT binary patch literal 2042 zcmdPZ#RCjYEDcNyOiav;EzK>=4J{1~j1A3AP0S4q%}p$Lxt#O!N>cMmbX_uw3-XII zOEUBGtQ6Ani*j{S5=#=T74i~uQ*D*fGILT5m8=y&G(;pZIXShUM6Vz(T?t`GNM%8) zl|p82VtT4RP==R_m#Z_t&z+Y`ii?4Pf!EW+B?w3hfG`Iekd*)YKM_c=q&xaLGBB(^ zVb0q163CY%U#85)$Dm{M7IGS!BGfoZ;{i(`nz>7|oj zdj}`V9RF)wcCIXknKQ*jv8g4XT2w{IdKT}|Jv!c(@9w>1mVCpf+o&k+n1`nB<||pn zez~u%Ji9i)0w?5owg>HY|MZD=BSU5a@7NFe*W{%KUc3SWO(#Xy`D$p2VZB= za{gDFZ8d$4J^q{cvcp2BRMjXbeUaVWec@YHE1aM0b=u~n;#~K(*^b=5rtMzzGA75Z z^R|Z ztvI%6(NxPB%%S&!1AN4t3_}^dhA7_2ZZ@cxTCic#oq5V0JDjAFntB8cFH7dMncisO ziZ`6%|3qg7|5Fudf4gJ%cO2LnkiwgLUOI7hwo8re$s;W8Z#kv;!}1oJEoi(JePmq< zL%001g|jq{+_)7W@^9l$+1&Gb2Mq#y_9dvZxFsp8O}Mk%?AP?&iC*DM zn0ihpBf`;p0pHs=;}u#-+1x9)v#|Yikb0wC9%!rBeydif^zze)s_hNiZn90`lWRXz z$Yhny_h$XBiFvj>t0vvBI+CItzoUPz)MoeQ_A{~}ea{@N=P+NLz~jX(&UfzE;-z{L z?OloipRZ|}D&F}vL1?yz)xO4#8Fx~FVRWrQ^4CSBInvV>PrchTxAp5er4Qbff?~_c zYfe>5dCW2rRLpE=d0S^(6(4cYI=}U3wo{b177JHgN08u`b@Mx|{JS)m7cS4@a9i-S zl~pTl#@Z7bE(R}cd9i-~$N10O=@-^Kt=x4b8JM0KJYD@<);T3K@p9qGsJgmT%c@4G zSv4hP0P|@+BA-r9lxY7Oy-lCBc}ZKO#tKc<2#x%kIbB_Tx2F|ncb)r|r#E-kS!Uh2 z!u+CJ7v9cTVr{{Hm^tmyl}oO#UI#4dRpQ7xyng>ZQD67d9dbWvDZ_bLpY z|0mzqE4f^HhOfURr?hj$Gfu~de~YZ{eAv@8DeAPx#SFvGE{%CnUR(UVoX%gX-Y6B- z^8Yj2HT$&*MhWegf|>Vq3O};l>8B#{r{(Cxm$ue?``#vgYvxwV66i1I+3H*oOHa` z%;V~F;nA9fF^tlGLyfJ%Lo=c$I-6;Cak7}{Nu`}qn5%W{fVAuv@3TAZ%ym1;(pC6x z;rG4VfAdRC?zGs;eoPS*`CJq6=kSS3OXZHS_?F#!=#s#5i&sc1{%Xr!g=Wbr&-cXoja&3b~#Ooe4 zu42QD$%kZ5uho$837eO8gg5BtVx~nYFS3?g4CGYTHT-qU@mz?bT`1?{ENK}Noh=0i z!Zs?I_^8OFo^#N1lqhog$vpE#q(Wj{ZbRi^hTiRhk+GVA79}!%JAAHxnR({Uyb`0` zpOvOA^yFN=bw2O>d|T(a!5dGcJk@*_za5zGWF6g3%QI+PK1lC;SCU#$0;~!_Sv)5{ oFI@q|2XV?XOEMIa@=Nnl5{oJ!viaGGIhon1#k#tb6bK;Q0IR$;-v9sr literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/firefox3-2pnglongtext/text.txt b/tests/scripts/multipart/firefox3-2pnglongtext/text.txt new file mode 100644 index 0000000..3bf804d --- /dev/null +++ b/tests/scripts/multipart/firefox3-2pnglongtext/text.txt @@ -0,0 +1,3 @@ +--long text +--with boundary +--lookalikes-- \ No newline at end of file diff --git a/tests/scripts/multipart/ie6-2png1txt/file1.png b/tests/scripts/multipart/ie6-2png1txt/file1.png new file mode 100644 index 0000000000000000000000000000000000000000..9b3422c61e5d23434d085834b82eed7a7363976e GIT binary patch literal 523 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 literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/ie6-2png1txt/text.txt b/tests/scripts/multipart/ie6-2png1txt/text.txt new file mode 100644 index 0000000..7c465b7 --- /dev/null +++ b/tests/scripts/multipart/ie6-2png1txt/text.txt @@ -0,0 +1 @@ +ie6 sucks :-/ \ No newline at end of file diff --git a/tests/scripts/multipart/ie7_full_path_request.txt b/tests/scripts/multipart/ie7_full_path_request.txt new file mode 100644 index 0000000000000000000000000000000000000000..acc4e2e1858f5888b7a9f64d47aae363145ffc90 GIT binary patch literal 30044 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 cM*|n+_b-i~S8HN)#RH5YnYUGP-mK>R8bw z2t8C_N?y`%B@?pVvm%P1(=O${na{Vw7Aotm+uS~f8Ro~wbIxHo`d;S7O>X_I!*}m4(;rQL3lRx0{4$S^45y5Zuub!<^(wd zDg1Ydq7a)PBNSH(^bYo1I&!_cv8F)Tfk3iCfaGzKcdABUEYJ~V4lNs7(L31l-D~h- zlLeyC;Rle^b>Pb0!o4OezZxjWAc9)nJe_NP=QDRgIAC4B#i)@%!f@-iPyu79xm3 z&=Ei~gdfA((>G@?t|CClgk~>+;;$k`$HzXCdi?6@>wW;(eSTM7w8{L??AcXUU@Ta{ zLrZBBKD>>5#ZOj$V zV$6R8et`Hsp^Y06t?}x?-BN{Jo+A{GLpF=}{yzmtYh)C=wic&;1MX^#S<^tIK8mw$ z9j5mS;s?n^1>U{8t&EN(863h(*qFLQ=;hn-R;7@>ex(0vvZCi*>q5p75xCt@M#zpM z*Z|q?@X7EteIjT$hYlRP@ApxiZy$PlmbH;D0hOKL_{*WZ;MR^|Dic+L1tK5@y>d4> zXDufE6I)H=Wxil#C-axq0i?$W%}gE53ocb3M9Ce~cq*pr4r68}P%}Rp!|ZI1=W3eKQ{(8&6eg2G=4KE}@YE!+_s=wG z)yj?Wi=|CWjtmfUw}tvT?>o~yi1Cr^9Fogm)I5&s5bN!p#FzaA_^&C@GMJ`fX;)}= z5K#fB0E~v<&l+~pTpQ_Hbc_JlkpsFSk>9$Ch@B!-R)IDix^hJgZHdi`3)**;A=W+} zt0nUFG38I&2bR2ge6xJwHJ&|-TDl&zbzpIUHhXn+ZJGP%0^v7ruA^itSBHJ0F6=jD>(Vqjq4_4IHF0@4B?%)tgEpWXX$^2@=JQ}@f$o;)eBWtvya_cwX@tTQ=kb3c{m+~33dOy=3Xa}O`~ z>wjKk^0bNi4l$(;rwf7(MlJG(Z(lY~*I%dPaH1tmzwyy}CAZ_p?Iu_KdG+db z9m9>gTe~DWQp%3z436btzBLMyM$o zn9fq-i%SxVfXO;PuPQa(aNrU)Q$8eNw@&u;2niHv*l+%RZ)LJ^n`eE>tX4y2L9s9I zl`_RWFD(ic5OEO+j1Hg4d{Hao@*=S?m#sWrr#2M?>T>;BC~$Vkkw4ttva-i#et)-j z`B9T*=ZN&;_qKE1KQ21AcUngPNA0&B7SpXiWMw-zB$_3fI~0yRFg11j&b{dT{0P=# ziH;pXM;|doOc8OZ+aRDAqQns*cR}i+)b=Rh+%ux*lg`w$)HVFDTi7ctxc1|#gLi{k zW9KQ0^gEk%|M+Lm>Y1G)vU{7@@;c^sS-p{$9hTU2ZO)Tj_MQ6?`$<>UdwX}U?demM zlsV*mW5zD$7f)}03|l&3U)_@b*_#$UkA3ZQOh|CY-2k@}eabf&Zod0)@`G8eQier4 zpUj^3-vj67%U4xT+d5I7aq8R$KTa>)^Wa)vqLy%I6RVN}+ukB`%XD3}zrxYJk5l(0 zq^f_OevNU-agMV-vS+#VLgigf%3iL}TK1YRIZZ9V*xqon@QgDikDfd5gjH~waXk-a zpP0|g-?7Vm%_C>4_|W!+hu`G%JOgf!##isAI!0Wg3j!gf{`eVK31)~|uk}|_H$^-%# zS1|OnJvc8_b}2^sWmHKI!;Oi|yObv0-__MIS1BZ{(M)bvkt=U*=h<%VTjf&~ui442 zR{iwV@Tvc8_Gx>nYBtwAGM#7J9koktOKC!3qgi!nN$$+wFPI*h=dW9wq8;AezcbnS z_hNau-V=XpSb~%^e)cX|w))M#kNJN-`CtCGc=L??Ha9%IYl5GAKCYcnrsGn_T+R_0 rU~oakh*tTnB(-8)c*)^C#N=sRU0yB#ZkE?q literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/opera8-2png1txt/text.txt b/tests/scripts/multipart/opera8-2png1txt/text.txt new file mode 100644 index 0000000..ca01cb0 --- /dev/null +++ b/tests/scripts/multipart/opera8-2png1txt/text.txt @@ -0,0 +1 @@ +blafasel öäü \ No newline at end of file diff --git a/tests/scripts/multipart/test_collect.py b/tests/scripts/multipart/test_collect.py new file mode 100644 index 0000000..004d800 --- /dev/null +++ b/tests/scripts/multipart/test_collect.py @@ -0,0 +1,56 @@ +""" +Hacky helper application to collect form data. +""" +from werkzeug.serving import run_simple +from werkzeug.wrappers import Request, Response + + +def copy_stream(request): + from os import mkdir + from time import time + folder = 'request-%d' % time() + mkdir(folder) + environ = request.environ + f = open(folder + '/request.txt', 'wb+') + f.write(environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))) + f.flush() + f.seek(0) + environ['wsgi.input'] = f + request.stat_folder = folder + + +def stats(request): + copy_stream(request) + f1 = request.files['file1'] + f2 = request.files['file2'] + text = request.form['text'] + f1.save(request.stat_folder + '/file1.bin') + f2.save(request.stat_folder + '/file2.bin') + with open(request.stat_folder + '/text.txt', 'w') as f: + f.write(text.encode('utf-8')) + return Response('Done.') + + +def upload_file(request): + return Response(''' +

Upload File

+
+
+
+
+ +
+ ''', mimetype='text/html') + + +def application(environ, start_responseonse): + request = Request(environ) + if request.method == 'POST': + response = stats(request) + else: + response = upload_file(request) + return response(environ, start_responseonse) + + +if __name__ == '__main__': + run_simple('localhost', 5000, application, use_debugger=True) diff --git a/tests/scripts/multipart/webkit3-2png1txt/file1.png b/tests/scripts/multipart/webkit3-2png1txt/file1.png new file mode 100644 index 0000000000000000000000000000000000000000..afca0732a75933c67a30ff16d2c9b4c3abf1cf1e GIT binary patch literal 1002 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 BEyDl+ literal 0 HcmV?d00001 diff --git a/tests/scripts/multipart/webkit3-2png1txt/text.txt b/tests/scripts/multipart/webkit3-2png1txt/text.txt new file mode 100644 index 0000000..baa1300 --- /dev/null +++ b/tests/scripts/multipart/webkit3-2png1txt/text.txt @@ -0,0 +1 @@ +this is another text with ümläüts \ No newline at end of file diff --git a/tests/scripts/res/test.txt b/tests/scripts/res/test.txt new file mode 100644 index 0000000..a8efdcc --- /dev/null +++ b/tests/scripts/res/test.txt @@ -0,0 +1 @@ +FOUND diff --git a/tests/scripts/run_tests.sh b/tests/scripts/run_tests.sh new file mode 100755 index 0000000..f2315d8 --- /dev/null +++ b/tests/scripts/run_tests.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash +set -eux + +pytest-3 ./test_wsgi.py ./test_formparser.py diff --git a/tests/scripts/test_formparser.py b/tests/scripts/test_formparser.py new file mode 100644 index 0000000..b0f53a0 --- /dev/null +++ b/tests/scripts/test_formparser.py @@ -0,0 +1,468 @@ +# -*- coding: utf-8 -*- +""" + tests.formparser + ~~~~~~~~~~~~~~~~ + + Tests the form parsing facilities. + + :copyright: (c) 2014 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement + +import pytest + +from os.path import join, dirname + +#from tests import strict_eq + +from werkzeug import formparser +from werkzeug.test import create_environ, Client +from werkzeug.wrappers import Request, Response +from werkzeug.exceptions import RequestEntityTooLarge +from werkzeug.datastructures import MultiDict +from werkzeug.formparser import parse_form_data, FormDataParser +from werkzeug._compat import BytesIO + + +def strict_eq(x, y): + '''Equality test bypassing the implicit string conversion in Python 2''' + __tracebackhide__ = True + assert x == y + assert issubclass(type(x), type(y)) or issubclass(type(y), type(x)) + if isinstance(x, dict) and isinstance(y, dict): + x = sorted(x.items()) + y = sorted(y.items()) + elif isinstance(x, set) and isinstance(y, set): + x = sorted(x) + y = sorted(y) + assert repr(x) == repr(y) + + +@Request.application +def form_data_consumer(request): + result_object = request.args['object'] + if result_object == 'text': + return Response(repr(request.form['text'])) + f = request.files[result_object] + return Response(b'\n'.join(( + repr(f.filename).encode('ascii'), + repr(f.name).encode('ascii'), + repr(f.content_type).encode('ascii'), + f.stream.read() + ))) + + +def get_contents(filename): + with open(filename, 'rb') as f: + return f.read() + + +class TestFormParser(object): + + def test_limiting(self): + data = b'foo=Hello+World&bar=baz' + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='application/x-www-form-urlencoded', + method='POST') + req.max_content_length = 400 + strict_eq(req.form['foo'], u'Hello World') + + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='application/x-www-form-urlencoded', + method='POST') + req.max_form_memory_size = 7 + pytest.raises(RequestEntityTooLarge, lambda: req.form['foo']) + + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='application/x-www-form-urlencoded', + method='POST') + req.max_form_memory_size = 400 + strict_eq(req.form['foo'], u'Hello World') + + data = (b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n' + b'Hello World\r\n' + b'--foo\r\nContent-Disposition: form-field; name=bar\r\n\r\n' + b'bar=baz\r\n--foo--') + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + req.max_content_length = 4 + pytest.raises(RequestEntityTooLarge, lambda: req.form['foo']) + + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + req.max_content_length = 400 + strict_eq(req.form['foo'], u'Hello World') + + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + req.max_form_memory_size = 7 + pytest.raises(RequestEntityTooLarge, lambda: req.form['foo']) + + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + req.max_form_memory_size = 400 + strict_eq(req.form['foo'], u'Hello World') + + # Test fix for CVE-2023-25577 that limits on form-data raise an error + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + req.max_form_parts = 1 + pytest.raises(RequestEntityTooLarge, lambda: req.form['foo']) + + + def test_missing_multipart_boundary(self): + data = (b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n' + b'Hello World\r\n' + b'--foo\r\nContent-Disposition: form-field; name=bar\r\n\r\n' + b'bar=baz\r\n--foo--') + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data', + method='POST') + assert req.form == {} + + def test_parse_form_data_put_without_content(self): + # A PUT without a Content-Type header returns empty data + + # Both rfc1945 and rfc2616 (1.0 and 1.1) say "Any HTTP/[1.0/1.1] message + # containing an entity-body SHOULD include a Content-Type header field + # defining the media type of that body." In the case where either + # headers are omitted, parse_form_data should still work. + env = create_environ('/foo', 'http://example.org/', method='PUT') + del env['CONTENT_TYPE'] + del env['CONTENT_LENGTH'] + + stream, form, files = formparser.parse_form_data(env) + strict_eq(stream.read(), b'') + strict_eq(len(form), 0) + strict_eq(len(files), 0) + + def test_parse_form_data_get_without_content(self): + env = create_environ('/foo', 'http://example.org/', method='GET') + del env['CONTENT_TYPE'] + del env['CONTENT_LENGTH'] + + stream, form, files = formparser.parse_form_data(env) + strict_eq(stream.read(), b'') + strict_eq(len(form), 0) + strict_eq(len(files), 0) + + def test_large_file(self): + data = b'x' * (1024 * 600) + req = Request.from_values(data={'foo': (BytesIO(data), 'test.txt')}, + method='POST') + # make sure we have a real file here, because we expect to be + # on the disk. > 1024 * 500 + assert hasattr(req.files['foo'].stream, u'fileno') + # close file to prevent fds from leaking + req.files['foo'].close() + + def test_streaming_parse(self): + data = b'x' * (1024 * 600) + + class StreamMPP(formparser.MultiPartParser): + + def parse(self, file, boundary, content_length): + i = iter(self.parse_lines(file, boundary, content_length, + cap_at_buffer=False)) + one = next(i) + two = next(i) + return self.cls(()), {'one': one, 'two': two} + + class StreamFDP(formparser.FormDataParser): + + def _sf_parse_multipart(self, stream, mimetype, + content_length, options): + form, files = StreamMPP( + self.stream_factory, self.charset, self.errors, + max_form_memory_size=self.max_form_memory_size, + cls=self.cls).parse(stream, options.get('boundary').encode('ascii'), + content_length) + return stream, form, files + parse_functions = {} + parse_functions.update(formparser.FormDataParser.parse_functions) + parse_functions['multipart/form-data'] = _sf_parse_multipart + + class StreamReq(Request): + form_data_parser_class = StreamFDP + req = StreamReq.from_values(data={'foo': (BytesIO(data), 'test.txt')}, + method='POST') + strict_eq('begin_file', req.files['one'][0]) + strict_eq(('foo', 'test.txt'), req.files['one'][1][1:]) + strict_eq('cont', req.files['two'][0]) + strict_eq(data, req.files['two'][1]) + + def test_parse_bad_content_type(self): + parser = FormDataParser() + assert parser.parse('', 'bad-mime-type', 0) == \ + ('', MultiDict([]), MultiDict([])) + + def test_parse_from_environ(self): + parser = FormDataParser() + stream, _, _ = parser.parse_from_environ({'wsgi.input': ''}) + assert stream is not None + + +class TestMultiPart(object): + + def test_basic(self): + resources = join(dirname(__file__), 'multipart') + client = Client(form_data_consumer, Response) + + repository = [ + ('firefox3-2png1txt', '---------------------------186454651713519341951581030105', [ + (u'anchor.png', 'file1', 'image/png', 'file1.png'), + (u'application_edit.png', 'file2', 'image/png', 'file2.png') + ], u'example text'), + ('firefox3-2pnglongtext', '---------------------------14904044739787191031754711748', [ + (u'accept.png', 'file1', 'image/png', 'file1.png'), + (u'add.png', 'file2', 'image/png', 'file2.png') + ], u'--long text\r\n--with boundary\r\n--lookalikes--'), + ('opera8-2png1txt', '----------zEO9jQKmLc2Cq88c23Dx19', [ + (u'arrow_branch.png', 'file1', 'image/png', 'file1.png'), + (u'award_star_bronze_1.png', 'file2', 'image/png', 'file2.png') + ], u'blafasel öäü'), + ('webkit3-2png1txt', '----WebKitFormBoundaryjdSFhcARk8fyGNy6', [ + (u'gtk-apply.png', 'file1', 'image/png', 'file1.png'), + (u'gtk-no.png', 'file2', 'image/png', 'file2.png') + ], u'this is another text with ümläüts'), + ('ie6-2png1txt', '---------------------------7d91b03a20128', [ + (u'file1.png', 'file1', 'image/x-png', 'file1.png'), + (u'file2.png', 'file2', 'image/x-png', 'file2.png') + ], u'ie6 sucks :-/') + ] + + for name, boundary, files, text in repository: + folder = join(resources, name) + data = get_contents(join(folder, 'request.txt')) + for filename, field, content_type, fsname in files: + response = client.post( + '/?object=' + field, + data=data, + content_type='multipart/form-data; boundary="%s"' % boundary, + content_length=len(data)) + lines = response.get_data().split(b'\n', 3) + strict_eq(lines[0], repr(filename).encode('ascii')) + strict_eq(lines[1], repr(field).encode('ascii')) + strict_eq(lines[2], repr(content_type).encode('ascii')) + strict_eq(lines[3], get_contents(join(folder, fsname))) + response = client.post( + '/?object=text', + data=data, + content_type='multipart/form-data; boundary="%s"' % boundary, + content_length=len(data)) + strict_eq(response.get_data(), repr(text).encode('utf-8')) + + def test_ie7_unc_path(self): + client = Client(form_data_consumer, Response) + data_file = join(dirname(__file__), 'multipart', 'ie7_full_path_request.txt') + data = get_contents(data_file) + boundary = '---------------------------7da36d1b4a0164' + response = client.post( + '/?object=cb_file_upload_multiple', + data=data, + content_type='multipart/form-data; boundary="%s"' % boundary, + content_length=len(data)) + lines = response.get_data().split(b'\n', 3) + strict_eq(lines[0], + repr(u'Sellersburg Town Council Meeting 02-22-2010doc.doc').encode('ascii')) + + def test_end_of_file(self): + # This test looks innocent but it was actually timeing out in + # the Werkzeug 0.5 release version (#394) + data = ( + b'--foo\r\n' + b'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n' + b'Content-Type: text/plain\r\n\r\n' + b'file contents and no end' + ) + data = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + assert not data.files + assert not data.form + + def test_broken(self): + data = ( + '--foo\r\n' + 'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n' + 'Content-Transfer-Encoding: base64\r\n' + 'Content-Type: text/plain\r\n\r\n' + 'broken base 64' + '--foo--' + ) + _, form, files = formparser.parse_form_data(create_environ( + data=data, method='POST', content_type='multipart/form-data; boundary=foo' + )) + assert not files + assert not form + + pytest.raises(ValueError, formparser.parse_form_data, + create_environ(data=data, method='POST', + content_type='multipart/form-data; boundary=foo'), + silent=False) + + def test_file_no_content_type(self): + data = ( + b'--foo\r\n' + b'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n\r\n' + b'file contents\r\n--foo--' + ) + data = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + assert data.files['test'].filename == 'test.txt' + strict_eq(data.files['test'].read(), b'file contents') + + def test_extra_newline(self): + # this test looks innocent but it was actually timeing out in + # the Werkzeug 0.5 release version (#394) + data = ( + b'\r\n\r\n--foo\r\n' + b'Content-Disposition: form-data; name="foo"\r\n\r\n' + b'a string\r\n' + b'--foo--' + ) + data = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + assert not data.files + strict_eq(data.form['foo'], u'a string') + + def test_headers(self): + data = (b'--foo\r\n' + b'Content-Disposition: form-data; name="foo"; filename="foo.txt"\r\n' + b'X-Custom-Header: blah\r\n' + b'Content-Type: text/plain; charset=utf-8\r\n\r\n' + b'file contents, just the contents\r\n' + b'--foo--') + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + foo = req.files['foo'] + strict_eq(foo.mimetype, 'text/plain') + strict_eq(foo.mimetype_params, {'charset': 'utf-8'}) + strict_eq(foo.headers['content-type'], foo.content_type) + strict_eq(foo.content_type, 'text/plain; charset=utf-8') + strict_eq(foo.headers['x-custom-header'], 'blah') + + def test_nonstandard_line_endings(self): + for nl in b'\n', b'\r', b'\r\n': + data = nl.join(( + b'--foo', + b'Content-Disposition: form-data; name=foo', + b'', + b'this is just bar', + b'--foo', + b'Content-Disposition: form-data; name=bar', + b'', + b'blafasel', + b'--foo--' + )) + req = Request.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; ' + 'boundary=foo', method='POST') + strict_eq(req.form['foo'], u'this is just bar') + strict_eq(req.form['bar'], u'blafasel') + + def test_failures(self): + def parse_multipart(stream, boundary, content_length): + parser = formparser.MultiPartParser(content_length) + return parser.parse(stream, boundary, content_length) + pytest.raises(ValueError, parse_multipart, BytesIO(), b'broken ', 0) + + data = b'--foo\r\n\r\nHello World\r\n--foo--' + pytest.raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data)) + + data = b'--foo\r\nContent-Disposition: form-field; name=foo\r\n' \ + b'Content-Transfer-Encoding: base64\r\n\r\nHello World\r\n--foo--' + pytest.raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data)) + + data = b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\nHello World\r\n' + pytest.raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data)) + + x = formparser.parse_multipart_headers(['foo: bar\r\n', ' x test\r\n']) + strict_eq(x['foo'], 'bar\n x test') + pytest.raises(ValueError, formparser.parse_multipart_headers, + ['foo: bar\r\n', ' x test']) + + def test_bad_newline_bad_newline_assumption(self): + class ISORequest(Request): + charset = 'latin1' + contents = b'U2vlbmUgbORu' + data = b'--foo\r\nContent-Disposition: form-data; name="test"\r\n' \ + b'Content-Transfer-Encoding: base64\r\n\r\n' + \ + contents + b'\r\n--foo--' + req = ISORequest.from_values(input_stream=BytesIO(data), + content_length=len(data), + content_type='multipart/form-data; boundary=foo', + method='POST') + strict_eq(req.form['test'], u'Sk\xe5ne l\xe4n') + + def test_empty_multipart(self): + environ = {} + data = b'--boundary--' + environ['REQUEST_METHOD'] = 'POST' + environ['CONTENT_TYPE'] = 'multipart/form-data; boundary=boundary' + environ['CONTENT_LENGTH'] = str(len(data)) + environ['wsgi.input'] = BytesIO(data) + stream, form, files = parse_form_data(environ, silent=False) + rv = stream.read() + assert rv == b'' + assert form == MultiDict() + assert files == MultiDict() + + +class TestMultiPartParser(object): + + def test_constructor_not_pass_stream_factory_and_cls(self): + parser = formparser.MultiPartParser() + + assert parser.stream_factory is formparser.default_stream_factory + assert parser.cls is MultiDict + + def test_constructor_pass_stream_factory_and_cls(self): + def stream_factory(): + pass + + parser = formparser.MultiPartParser(stream_factory=stream_factory, cls=dict) + + assert parser.stream_factory is stream_factory + assert parser.cls is dict + + +class TestInternalFunctions(object): + + def test_line_parser(self): + assert formparser._line_parse('foo') == ('foo', False) + assert formparser._line_parse('foo\r\n') == ('foo', True) + assert formparser._line_parse('foo\r') == ('foo', True) + assert formparser._line_parse('foo\n') == ('foo', True) + + def test_find_terminator(self): + lineiter = iter(b'\n\n\nfoo\nbar\nbaz'.splitlines(True)) + find_terminator = formparser.MultiPartParser()._find_terminator + line = find_terminator(lineiter) + assert line == b'foo' + assert list(lineiter) == [b'bar\n', b'baz'] + assert find_terminator([]) == b'' + assert find_terminator([b'']) == b'' diff --git a/tests/scripts/test_wsgi.py b/tests/scripts/test_wsgi.py new file mode 100644 index 0000000..a1c50c5 --- /dev/null +++ b/tests/scripts/test_wsgi.py @@ -0,0 +1,474 @@ +# -*- coding: utf-8 -*- +""" + tests.wsgi + ~~~~~~~~~~ + + Tests the WSGI utilities. + + :copyright: (c) 2014 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os + +import pytest + +from os import path +from contextlib import closing + +#from tests import strict_eq + +from werkzeug.wrappers import BaseResponse +from werkzeug.exceptions import BadRequest, ClientDisconnected +from werkzeug.test import Client, create_environ, run_wsgi_app +from werkzeug import wsgi +from werkzeug._compat import StringIO, BytesIO, NativeStringIO, to_native, \ + to_bytes +from werkzeug.wsgi import _RangeWrapper, wrap_file + + +def strict_eq(x, y): + '''Equality test bypassing the implicit string conversion in Python 2''' + __tracebackhide__ = True + assert x == y + assert issubclass(type(x), type(y)) or issubclass(type(y), type(x)) + if isinstance(x, dict) and isinstance(y, dict): + x = sorted(x.items()) + y = sorted(y.items()) + elif isinstance(x, set) and isinstance(y, set): + x = sorted(x) + y = sorted(y) + assert repr(x) == repr(y) + + +def test_shareddatamiddleware_get_file_loader(): + app = wsgi.SharedDataMiddleware(None, {}) + assert callable(app.get_file_loader('foo')) + + +def test_shared_data_middleware(tmpdir): + def null_application(environ, start_response): + start_response('404 NOT FOUND', [('Content-Type', 'text/plain')]) + yield b'NOT FOUND' + + test_dir = str(tmpdir) + with open(path.join(test_dir, to_native(u'äöü', 'utf-8')), 'w') as test_file: + test_file.write(u'FOUND') + + app = wsgi.SharedDataMiddleware(null_application, { + '/': path.join(path.dirname(__file__), 'res'), + '/sources': path.join(path.dirname(__file__), 'res'), + '/pkg': ('werkzeug.debug', 'shared'), + '/foo': test_dir + }) + + for p in '/test.txt', '/sources/test.txt', '/foo/äöü': + app_iter, status, headers = run_wsgi_app(app, create_environ(p)) + assert status == '200 OK' + with closing(app_iter) as app_iter: + data = b''.join(app_iter).strip() + assert data == b'FOUND' + + app_iter, status, headers = run_wsgi_app( + app, create_environ('/pkg/debugger.js')) + with closing(app_iter) as app_iter: + contents = b''.join(app_iter) + assert b'$(function() {' in contents + + app_iter, status, headers = run_wsgi_app( + app, create_environ('/missing')) + assert status == '404 NOT FOUND' + assert b''.join(app_iter).strip() == b'NOT FOUND' + + +def test_dispatchermiddleware(): + def null_application(environ, start_response): + start_response('404 NOT FOUND', [('Content-Type', 'text/plain')]) + yield b'NOT FOUND' + + def dummy_application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + yield to_bytes(environ['SCRIPT_NAME']) + + app = wsgi.DispatcherMiddleware(null_application, { + '/test1': dummy_application, + '/test2/very': dummy_application, + }) + tests = { + '/test1': ('/test1', '/test1/asfd', '/test1/very'), + '/test2/very': ('/test2/very', '/test2/very/long/path/after/script/name') + } + for name, urls in tests.items(): + for p in urls: + environ = create_environ(p) + app_iter, status, headers = run_wsgi_app(app, environ) + assert status == '200 OK' + assert b''.join(app_iter).strip() == to_bytes(name) + + app_iter, status, headers = run_wsgi_app( + app, create_environ('/missing')) + assert status == '404 NOT FOUND' + assert b''.join(app_iter).strip() == b'NOT FOUND' + + +def test_get_host(): + env = {'HTTP_X_FORWARDED_HOST': 'example.org', + 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} + assert wsgi.get_host(env) == 'example.org' + assert wsgi.get_host(create_environ('/', 'http://example.org')) == \ + 'example.org' + + +def test_get_host_multiple_forwarded(): + env = {'HTTP_X_FORWARDED_HOST': 'example.com, example.org', + 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} + assert wsgi.get_host(env) == 'example.com' + assert wsgi.get_host(create_environ('/', 'http://example.com')) == \ + 'example.com' + + +def test_get_host_validation(): + env = {'HTTP_X_FORWARDED_HOST': 'example.org', + 'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'} + assert wsgi.get_host(env, trusted_hosts=['.example.org']) == 'example.org' + pytest.raises(BadRequest, wsgi.get_host, env, + trusted_hosts=['example.com']) + + +def test_responder(): + def foo(environ, start_response): + return BaseResponse(b'Test') + client = Client(wsgi.responder(foo), BaseResponse) + response = client.get('/') + assert response.status_code == 200 + assert response.data == b'Test' + + +def test_pop_path_info(): + original_env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b///c'} + + # regular path info popping + def assert_tuple(script_name, path_info): + assert env.get('SCRIPT_NAME') == script_name + assert env.get('PATH_INFO') == path_info + env = original_env.copy() + pop = lambda: wsgi.pop_path_info(env) + + assert_tuple('/foo', '/a/b///c') + assert pop() == 'a' + assert_tuple('/foo/a', '/b///c') + assert pop() == 'b' + assert_tuple('/foo/a/b', '///c') + assert pop() == 'c' + assert_tuple('/foo/a/b///c', '') + assert pop() is None + + +def test_peek_path_info(): + env = { + 'SCRIPT_NAME': '/foo', + 'PATH_INFO': '/aaa/b///c' + } + + assert wsgi.peek_path_info(env) == 'aaa' + assert wsgi.peek_path_info(env) == 'aaa' + assert wsgi.peek_path_info(env, charset=None) == b'aaa' + assert wsgi.peek_path_info(env, charset=None) == b'aaa' + + +def test_path_info_and_script_name_fetching(): + env = create_environ(u'/\N{SNOWMAN}', u'http://example.com/\N{COMET}/') + assert wsgi.get_path_info(env) == u'/\N{SNOWMAN}' + assert wsgi.get_path_info(env, charset=None) == u'/\N{SNOWMAN}'.encode('utf-8') + assert wsgi.get_script_name(env) == u'/\N{COMET}' + assert wsgi.get_script_name(env, charset=None) == u'/\N{COMET}'.encode('utf-8') + + +def test_query_string_fetching(): + env = create_environ(u'/?\N{SNOWMAN}=\N{COMET}') + qs = wsgi.get_query_string(env) + strict_eq(qs, '%E2%98%83=%E2%98%84') + + +def test_limited_stream(): + class RaisingLimitedStream(wsgi.LimitedStream): + + def on_exhausted(self): + raise BadRequest('input stream exhausted') + + io = BytesIO(b'123456') + stream = RaisingLimitedStream(io, 3) + strict_eq(stream.read(), b'123') + pytest.raises(BadRequest, stream.read) + + io = BytesIO(b'123456') + stream = RaisingLimitedStream(io, 3) + strict_eq(stream.tell(), 0) + strict_eq(stream.read(1), b'1') + strict_eq(stream.tell(), 1) + strict_eq(stream.read(1), b'2') + strict_eq(stream.tell(), 2) + strict_eq(stream.read(1), b'3') + strict_eq(stream.tell(), 3) + pytest.raises(BadRequest, stream.read) + + io = BytesIO(b'123456\nabcdefg') + stream = wsgi.LimitedStream(io, 9) + strict_eq(stream.readline(), b'123456\n') + strict_eq(stream.readline(), b'ab') + + io = BytesIO(b'123456\nabcdefg') + stream = wsgi.LimitedStream(io, 9) + strict_eq(stream.readlines(), [b'123456\n', b'ab']) + + io = BytesIO(b'123456\nabcdefg') + stream = wsgi.LimitedStream(io, 9) + strict_eq(stream.readlines(2), [b'12']) + strict_eq(stream.readlines(2), [b'34']) + strict_eq(stream.readlines(), [b'56\n', b'ab']) + + io = BytesIO(b'123456\nabcdefg') + stream = wsgi.LimitedStream(io, 9) + strict_eq(stream.readline(100), b'123456\n') + + io = BytesIO(b'123456\nabcdefg') + stream = wsgi.LimitedStream(io, 9) + strict_eq(stream.readlines(100), [b'123456\n', b'ab']) + + io = BytesIO(b'123456') + stream = wsgi.LimitedStream(io, 3) + strict_eq(stream.read(1), b'1') + strict_eq(stream.read(1), b'2') + strict_eq(stream.read(), b'3') + strict_eq(stream.read(), b'') + + io = BytesIO(b'123456') + stream = wsgi.LimitedStream(io, 3) + strict_eq(stream.read(-1), b'123') + + io = BytesIO(b'123456') + stream = wsgi.LimitedStream(io, 0) + strict_eq(stream.read(-1), b'') + + io = StringIO(u'123456') + stream = wsgi.LimitedStream(io, 0) + strict_eq(stream.read(-1), u'') + + io = StringIO(u'123\n456\n') + stream = wsgi.LimitedStream(io, 8) + strict_eq(list(stream), [u'123\n', u'456\n']) + + +def test_limited_stream_disconnection(): + io = BytesIO(b'A bit of content') + + # disconnect detection on out of bytes + stream = wsgi.LimitedStream(io, 255) + with pytest.raises(ClientDisconnected): + stream.read() + + # disconnect detection because file close + io = BytesIO(b'x' * 255) + io.close() + stream = wsgi.LimitedStream(io, 255) + with pytest.raises(ClientDisconnected): + stream.read() + + +def test_path_info_extraction(): + x = wsgi.extract_path_info('http://example.com/app', '/app/hello') + assert x == u'/hello' + x = wsgi.extract_path_info('http://example.com/app', + 'https://example.com/app/hello') + assert x == u'/hello' + x = wsgi.extract_path_info('http://example.com/app/', + 'https://example.com/app/hello') + assert x == u'/hello' + x = wsgi.extract_path_info('http://example.com/app/', + 'https://example.com/app') + assert x == u'/' + x = wsgi.extract_path_info(u'http://☃.net/', u'/fööbär') + assert x == u'/fööbär' + x = wsgi.extract_path_info(u'http://☃.net/x', u'http://☃.net/x/fööbär') + assert x == u'/fööbär' + + env = create_environ(u'/fööbär', u'http://☃.net/x/') + x = wsgi.extract_path_info(env, u'http://☃.net/x/fööbär') + assert x == u'/fööbär' + + x = wsgi.extract_path_info('http://example.com/app/', + 'https://example.com/a/hello') + assert x is None + x = wsgi.extract_path_info('http://example.com/app/', + 'https://example.com/app/hello', + collapse_http_schemes=False) + assert x is None + + +def test_get_host_fallback(): + assert wsgi.get_host({ + 'SERVER_NAME': 'foobar.example.com', + 'wsgi.url_scheme': 'http', + 'SERVER_PORT': '80' + }) == 'foobar.example.com' + assert wsgi.get_host({ + 'SERVER_NAME': 'foobar.example.com', + 'wsgi.url_scheme': 'http', + 'SERVER_PORT': '81' + }) == 'foobar.example.com:81' + + +def test_get_current_url_unicode(): + env = create_environ() + env['QUERY_STRING'] = 'foo=bar&baz=blah&meh=\xcf' + rv = wsgi.get_current_url(env) + strict_eq(rv, + u'http://localhost/?foo=bar&baz=blah&meh=\ufffd') + + +def test_multi_part_line_breaks(): + data = 'abcdef\r\nghijkl\r\nmnopqrstuvwxyz\r\nABCDEFGHIJK' + test_stream = NativeStringIO(data) + lines = list(wsgi.make_line_iter(test_stream, limit=len(data), + buffer_size=16)) + assert lines == ['abcdef\r\n', 'ghijkl\r\n', 'mnopqrstuvwxyz\r\n', + 'ABCDEFGHIJK'] + + data = 'abc\r\nThis line is broken by the buffer length.' \ + '\r\nFoo bar baz' + test_stream = NativeStringIO(data) + lines = list(wsgi.make_line_iter(test_stream, limit=len(data), + buffer_size=24)) + assert lines == ['abc\r\n', 'This line is broken by the buffer ' + 'length.\r\n', 'Foo bar baz'] + + +def test_multi_part_line_breaks_bytes(): + data = b'abcdef\r\nghijkl\r\nmnopqrstuvwxyz\r\nABCDEFGHIJK' + test_stream = BytesIO(data) + lines = list(wsgi.make_line_iter(test_stream, limit=len(data), + buffer_size=16)) + assert lines == [b'abcdef\r\n', b'ghijkl\r\n', b'mnopqrstuvwxyz\r\n', + b'ABCDEFGHIJK'] + + data = b'abc\r\nThis line is broken by the buffer length.' \ + b'\r\nFoo bar baz' + test_stream = BytesIO(data) + lines = list(wsgi.make_line_iter(test_stream, limit=len(data), + buffer_size=24)) + assert lines == [b'abc\r\n', b'This line is broken by the buffer ' + b'length.\r\n', b'Foo bar baz'] + + +def test_multi_part_line_breaks_problematic(): + data = 'abc\rdef\r\nghi' + for x in range(1, 10): + test_stream = NativeStringIO(data) + lines = list(wsgi.make_line_iter(test_stream, limit=len(data), + buffer_size=4)) + assert lines == ['abc\r', 'def\r\n', 'ghi'] + + +def test_iter_functions_support_iterators(): + data = ['abcdef\r\nghi', 'jkl\r\nmnopqrstuvwxyz\r', '\nABCDEFGHIJK'] + lines = list(wsgi.make_line_iter(data)) + assert lines == ['abcdef\r\n', 'ghijkl\r\n', 'mnopqrstuvwxyz\r\n', + 'ABCDEFGHIJK'] + + +def test_make_chunk_iter(): + data = [u'abcdefXghi', u'jklXmnopqrstuvwxyzX', u'ABCDEFGHIJK'] + rv = list(wsgi.make_chunk_iter(data, 'X')) + assert rv == [u'abcdef', u'ghijkl', u'mnopqrstuvwxyz', u'ABCDEFGHIJK'] + + data = u'abcdefXghijklXmnopqrstuvwxyzXABCDEFGHIJK' + test_stream = StringIO(data) + rv = list(wsgi.make_chunk_iter(test_stream, 'X', limit=len(data), + buffer_size=4)) + assert rv == [u'abcdef', u'ghijkl', u'mnopqrstuvwxyz', u'ABCDEFGHIJK'] + + +def test_make_chunk_iter_bytes(): + data = [b'abcdefXghi', b'jklXmnopqrstuvwxyzX', b'ABCDEFGHIJK'] + rv = list(wsgi.make_chunk_iter(data, 'X')) + assert rv == [b'abcdef', b'ghijkl', b'mnopqrstuvwxyz', b'ABCDEFGHIJK'] + + data = b'abcdefXghijklXmnopqrstuvwxyzXABCDEFGHIJK' + test_stream = BytesIO(data) + rv = list(wsgi.make_chunk_iter(test_stream, 'X', limit=len(data), + buffer_size=4)) + assert rv == [b'abcdef', b'ghijkl', b'mnopqrstuvwxyz', b'ABCDEFGHIJK'] + + data = b'abcdefXghijklXmnopqrstuvwxyzXABCDEFGHIJK' + test_stream = BytesIO(data) + rv = list(wsgi.make_chunk_iter(test_stream, 'X', limit=len(data), + buffer_size=4, cap_at_buffer=True)) + assert rv == [b'abcd', b'ef', b'ghij', b'kl', b'mnop', b'qrst', b'uvwx', + b'yz', b'ABCD', b'EFGH', b'IJK'] + + +def test_lines_longer_buffer_size(): + data = '1234567890\n1234567890\n' + for bufsize in range(1, 15): + lines = list(wsgi.make_line_iter(NativeStringIO(data), limit=len(data), + buffer_size=4)) + assert lines == ['1234567890\n', '1234567890\n'] + + +def test_lines_longer_buffer_size_cap(): + data = '1234567890\n1234567890\n' + for bufsize in range(1, 15): + lines = list(wsgi.make_line_iter(NativeStringIO(data), limit=len(data), + buffer_size=4, cap_at_buffer=True)) + assert lines == ['1234', '5678', '90\n', '1234', '5678', '90\n'] + + +def test_range_wrapper(): + response = BaseResponse(b'Hello World') + range_wrapper = _RangeWrapper(response.response, 6, 4) + assert next(range_wrapper) == b'Worl' + + response = BaseResponse(b'Hello World') + range_wrapper = _RangeWrapper(response.response, 1, 0) + with pytest.raises(StopIteration): + next(range_wrapper) + + response = BaseResponse(b'Hello World') + range_wrapper = _RangeWrapper(response.response, 6, 100) + assert next(range_wrapper) == b'World' + + response = BaseResponse((x for x in (b'He', b'll', b'o ', b'Wo', b'rl', b'd'))) + range_wrapper = _RangeWrapper(response.response, 6, 4) + assert not range_wrapper.seekable + assert next(range_wrapper) == b'Wo' + assert next(range_wrapper) == b'rl' + + response = BaseResponse((x for x in (b'He', b'll', b'o W', b'o', b'rld'))) + range_wrapper = _RangeWrapper(response.response, 6, 4) + assert next(range_wrapper) == b'W' + assert next(range_wrapper) == b'o' + assert next(range_wrapper) == b'rl' + with pytest.raises(StopIteration): + next(range_wrapper) + + response = BaseResponse((x for x in (b'Hello', b' World'))) + range_wrapper = _RangeWrapper(response.response, 1, 1) + assert next(range_wrapper) == b'e' + with pytest.raises(StopIteration): + next(range_wrapper) + + resources = os.path.join(os.path.dirname(__file__), 'res') + env = create_environ() + with open(os.path.join(resources, 'test.txt'), 'rb') as f: + response = BaseResponse(wrap_file(env, f)) + range_wrapper = _RangeWrapper(response.response, 1, 2) + assert range_wrapper.seekable + assert next(range_wrapper) == b'OU' + with pytest.raises(StopIteration): + next(range_wrapper) + + with open(os.path.join(resources, 'test.txt'), 'rb') as f: + response = BaseResponse(wrap_file(env, f)) + range_wrapper = _RangeWrapper(response.response, 2) + assert next(range_wrapper) == b'UND\n' + with pytest.raises(StopIteration): + next(range_wrapper) diff --git a/tests/tests.yml b/tests/tests.yml new file mode 100644 index 0000000..a1fd2b2 --- /dev/null +++ b/tests/tests.yml @@ -0,0 +1,15 @@ +--- +# Run Werkzeug wsgi and formparser tests +- hosts: localhost + roles: + - role: standard-test-basic + tags: + - classic + + required_packages: + - python3-pytest + + tests: + - simple: + dir: scripts + run: ./run_tests.sh