Skip to content

Commit 4bf646f

Browse files
committed
Fix multipart form parsing in FieldStorage.
Prepend the Content-Type header (with boundary) before parsing multipart POST bodies so admin membership forms and other file-upload pages actually receive their submitted fields.
1 parent 9524275 commit 4bf646f

2 files changed

Lines changed: 76 additions & 4 deletions

File tree

Mailman/Utils.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,15 @@ def _parse_urlencoded_post(self):
142142
def _parse_multipart_post(self):
143143
"""Parse multipart/form-data POST data."""
144144
content_length = int(self.environ.get('CONTENT_LENGTH', 0))
145-
if content_length > 0:
145+
content_type = self.environ.get('CONTENT_TYPE', '')
146+
if content_length > 0 and content_type:
146147
post_data = sys.stdin.buffer.read(content_length)
147-
148-
# Parse the multipart message
148+
149+
# The email parser needs the Content-Type header (with boundary);
150+
# the raw POST body from the web server contains only the parts.
149151
parser = BytesParser(policy=HTTP)
150-
msg = parser.parsebytes(post_data)
152+
header = ('Content-Type: %s\r\n\r\n' % content_type).encode('latin-1')
153+
msg = parser.parsebytes(header + post_data)
151154

152155
for part in msg.walk():
153156
if part.get_content_maintype() == 'multipart':

tests/test_fieldstorage.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (C) 2001-2018 by the Free Software Foundation, Inc.
2+
#
3+
# This program is free software; you can redistribute it and/or
4+
# modify it under the terms of the GNU General Public License
5+
# as published by the Free Software Foundation; either version 2
6+
# of the License, or (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program; if not, write to the Free Software
15+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16+
# USA.
17+
18+
"""Unit tests for Mailman.Utils.FieldStorage multipart parsing."""
19+
20+
import io
21+
import os
22+
import sys
23+
import unittest
24+
25+
try:
26+
from Mailman import __init__
27+
except ImportError:
28+
import paths
29+
30+
from Mailman.Utils import FieldStorage
31+
32+
33+
def _multipart_body(boundary, fields):
34+
parts = []
35+
for name, value in fields:
36+
parts.append(
37+
'--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n'
38+
% (boundary, name, value))
39+
parts.append('--%s--\r\n' % boundary)
40+
return ''.join(parts).encode()
41+
42+
43+
class TestFieldStorageMultipart(unittest.TestCase):
44+
def _parse(self, fields):
45+
boundary = '----TestBoundary'
46+
body = _multipart_body(boundary, fields)
47+
environ = {
48+
'REQUEST_METHOD': 'POST',
49+
'CONTENT_TYPE': 'multipart/form-data; boundary=%s' % boundary,
50+
'CONTENT_LENGTH': str(len(body)),
51+
}
52+
sys.stdin = io.BytesIO(body)
53+
return FieldStorage(keep_blank_values=1, environ=environ)
54+
55+
def test_member_unsub_fields(self):
56+
cgidata = self._parse([
57+
('jackplimpton%40gmail.com_unsub', 'off'),
58+
('user', 'jackplimpton%40gmail.com'),
59+
('setmemberopts_btn', 'Submit Your Changes'),
60+
])
61+
self.assertIn('setmemberopts_btn', cgidata)
62+
self.assertIn('user', cgidata)
63+
self.assertIn('jackplimpton%40gmail.com_unsub', cgidata)
64+
self.assertEqual(
65+
cgidata.getfirst('jackplimpton%40gmail.com_unsub'), 'off')
66+
67+
68+
if __name__ == '__main__':
69+
unittest.main()

0 commit comments

Comments
 (0)