Skip to content

Commit 6e35fc1

Browse files
committed
fix: issue2551387 - TypeError: not indexable.
Fix crash due to uninitialized list element on a (Mini)FieldStorage when unexpected input is posted via wsgi. This doesn't happen when running roundup-server. It might happen under other front ends. Moved the code that sets '.list = [] if .list == None' to the main flow. Added an exception hander that logs the value of self.form if self.form.list raises an AttributeError. This exception should never happen if I understand the code correctly (but I probably don't). Fixed a number of test cases that were broken because I was calling Client and passing '[]' rather than a cgi.formStorage object. Added test cases: create a FileStorage (self.form) with .list = None. check AttributeError exception and verify logging. Problem reported and debugged by Christof Meerwald.
1 parent e150d35 commit 6e35fc1

File tree

6 files changed

+125
-20
lines changed

6 files changed

+125
-20
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Fixed:
6767
result, incorrectly formatted CORS preflight requests
6868
(e.g. missing Origin header) can now return HTTP status 403 as
6969
well as status 400. (John Rouillard)
70+
- issue2551387 - TypeError: not indexable. Fix crash due to
71+
uninitialized list element on a (Mini)FieldStorage when unexpected
72+
input is posted via wsgi. (Reported and debugged by Christof
73+
Meerwald; fix John Rouillard)
7074

7175
Features:
7276

roundup/cgi/client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -504,13 +504,26 @@ def __init__(self, instance, request, env, form=None, translator=None):
504504
# It's a workaround for a bug in cgi.FieldStorage. See class
505505
# def for details.
506506
self.form = BinaryFieldStorage(fp=request.rfile, environ=env)
507-
# In some case (e.g. content-type application/xml), cgi
508-
# will not parse anything. Fake a list property in this case
509-
if self.form.list is None:
510-
self.form.list = []
511507
else:
512508
self.form = form
513509

510+
# When the CONTENT-TYPE is not 'application/x-www-form-urlencoded':
511+
# or multipart/*, cgi.(Mini)FieldStorage sets the list property to
512+
# None. Initialize an empty list property in this case so we can
513+
# query the list in all cases.
514+
try:
515+
if (self.form.list is None):
516+
self.form.list = []
517+
except AttributeError:
518+
# self.form should always be some type of
519+
# FieldStorage. If we get an AttributeError,
520+
# print what the form is.
521+
# FIXME: plan on removing this in 2028 to improve
522+
# performance if there are no reports of it being triggered.
523+
logger.error(("Invalid self.form found (please report "
524+
"to the roundup-users mailing list): %s") % self.form)
525+
raise
526+
514527
# turn debugging on/off
515528
try:
516529
self.debug = int(env.get("ROUNDUP_DEBUG", 0))

test/db_test_base.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@
3030
from email import message_from_string
3131

3232
import pytest
33+
34+
try:
35+
from StringIO import cStringIO as IOBuff
36+
except ImportError:
37+
# python 3
38+
from io import BytesIO as IOBuff
39+
40+
from roundup.cgi.client import BinaryFieldStorage
3341
from roundup.hyperdb import String, Password, Link, Multilink, Date, \
3442
Interval, DatabaseError, Boolean, Number, Node, Integer
3543
from roundup.mailer import Mailer
@@ -43,7 +51,7 @@
4351
from roundup.exceptions import UsageError, Reject
4452
from roundup.mlink_expr import ExpressionError
4553

46-
from roundup.anypy.strings import b2s, s2b, u2s
54+
from roundup.anypy.strings import b2s, bs2b, s2b, u2s
4755
from roundup.anypy.cmp_ import NoneAndDictComparable
4856
from roundup.anypy.email_ import message_from_bytes
4957

@@ -4340,6 +4348,14 @@ def testHTMLItemDerefFail(self):
43404348
ae(bool(issue ['priority']['name']),False)
43414349

43424350
def makeForm(args):
4351+
""" Takes a dict of form elements or a FieldStorage.
4352+
4353+
FieldStorage is just returned so you can pass the result from
4354+
makeFormFromString through it cleanly.
4355+
"""
4356+
if isinstance(args, cgi.FieldStorage):
4357+
return args
4358+
43434359
form = cgi.FieldStorage()
43444360
for k,v in args.items():
43454361
if type(v) is type([]):
@@ -4352,6 +4368,33 @@ def makeForm(args):
43524368
form.list.append(cgi.MiniFieldStorage(k, v))
43534369
return form
43544370

4371+
def makeFormFromString(bstring, env=None):
4372+
"""used for generating a form that looks like a rest or xmlrpc
4373+
payload. Takes a binary or regular string and stores
4374+
it in a FieldStorage object.
4375+
4376+
:param: bstring - binary or regular string
4377+
:param" env - dict of environment variables. Names/keys
4378+
must be uppercase.
4379+
e.g. {"REQUEST_METHOD": "DELETE"}
4380+
4381+
Ideally this would be part of makeForm, but the env dict
4382+
is needed here to allow FieldStorage to properly define
4383+
the resulting object.
4384+
"""
4385+
rfile=IOBuff(bs2b(bstring))
4386+
4387+
# base headers required for BinaryFieldStorage/FieldStorage
4388+
e = {'REQUEST_METHOD':'POST',
4389+
'CONTENT_TYPE': 'text/xml',
4390+
'CONTENT_LENGTH': str(len(bs2b(bstring)))
4391+
}
4392+
if env:
4393+
e.update(env)
4394+
4395+
form = BinaryFieldStorage(fp=rfile, environ=e)
4396+
return form
4397+
43554398
class FileUpload:
43564399
def __init__(self, content, filename):
43574400
self.content = content

test/rest_common.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
import sys
55
import errno
6+
import logging
67

78
from time import sleep
89
from datetime import datetime, timedelta
@@ -76,6 +77,10 @@ def dst(self, dt):
7677

7778
class TestCase():
7879

80+
@pytest.fixture(autouse=True)
81+
def inject_fixtures(self, caplog):
82+
self._caplog = caplog
83+
7984
backend = None
8085
url_pfx = 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/'
8186

@@ -291,7 +296,8 @@ def setUp(self):
291296
'HTTP_ORIGIN': 'http://tracker.example'
292297
}
293298
self.dummy_client = client.Client(self.instance, MockNull(),
294-
self.client_env, [], None)
299+
self.client_env,
300+
cgi.FieldStorage(), None)
295301
self.dummy_client.request.headers.get = self.get_header
296302
self.dummy_client.db = self.db
297303

@@ -2294,6 +2300,30 @@ def testdetermine_output_formatBadAccept(self):
22942300
self.assertNotIn("X-Content-Type-Options",
22952301
self.server.client.additional_headers)
22962302

2303+
def testBadFormAttributeErrorException(self):
2304+
env = {
2305+
'PATH_INFO': 'rest/data/user',
2306+
'HTTP_HOST': 'localhost',
2307+
'TRACKER_NAME': 'rounduptest',
2308+
"REQUEST_METHOD": "GET"
2309+
}
2310+
2311+
2312+
with self._caplog.at_level(logging.ERROR, logger="roundup"):
2313+
with self.assertRaises(AttributeError) as exc:
2314+
self.dummy_client = client.Client(
2315+
self.instance, MockNull(), env, [], None)
2316+
2317+
self.assertEqual(exc.exception.args[0],
2318+
"'list' object has no attribute 'list'")
2319+
2320+
# log should look like (with string not broken into parts):
2321+
# [('roundup', 40,
2322+
# 'Invalid self.form found (please report to the '
2323+
# 'roundup-users mailing list): []')]
2324+
log = self._caplog.record_tuples[:]
2325+
self.assertIn("Invalid self.form found", log[0][2])
2326+
22972327
def testDispatchBadAccept(self):
22982328
# simulate: /rest/data/issue expect failure unknown accept settings
22992329
body=b'{ "title": "Joe Doe has problems", \
@@ -4508,16 +4538,16 @@ def wh(s):
45084538
'TRACKER_NAME': 'rounduptest',
45094539
"REQUEST_METHOD": "GET"
45104540
}
4541+
45114542
self.dummy_client = client.Client(self.instance, MockNull(), env,
4512-
[], None)
4543+
cgi.FieldStorage(), None)
45134544
self.dummy_client.db = self.db
45144545
self.dummy_client.request.headers.get = self.get_header
45154546
self.empty_form = cgi.FieldStorage()
45164547
self.terse_form = cgi.FieldStorage()
45174548
self.terse_form.list = [
45184549
cgi.MiniFieldStorage('@verbose', '0'),
45194550
]
4520-
self.dummy_client.form = cgi.FieldStorage()
45214551
self.dummy_client.form.list = [
45224552
cgi.MiniFieldStorage('@fields', 'username,address'),
45234553
]
@@ -4575,7 +4605,7 @@ def wh(s):
45754605
)
45764606

45774607
self.dummy_client = client.Client(self.instance, MockNull(), env,
4578-
[], None)
4608+
cgi.FieldStorage(), None)
45794609
self.dummy_client.db = self.db
45804610
self.dummy_client.request.headers.get = self.get_header
45814611
self.empty_form = cgi.FieldStorage()
@@ -4645,7 +4675,7 @@ def wh(s):
46454675
}
46464676

46474677
self.dummy_client = client.Client(self.instance, MockNull(), env,
4648-
[], None)
4678+
cgi.FieldStorage(), None)
46494679
self.dummy_client.db = self.db
46504680
self.dummy_client.request.headers.get = self.get_header
46514681
self.empty_form = cgi.FieldStorage()
@@ -4710,7 +4740,7 @@ def wh(s):
47104740
"REQUEST_METHOD": "GET"
47114741
}
47124742
self.dummy_client = client.Client(self.instance, MockNull(), env,
4713-
[], None)
4743+
cgi.FieldStorage(), None)
47144744
self.dummy_client.db = self.db
47154745
self.dummy_client.request.headers.get = self.get_header
47164746
self.empty_form = cgi.FieldStorage()
@@ -4781,7 +4811,7 @@ def wh(s):
47814811
"REQUEST_METHOD": "GET"
47824812
}
47834813
self.dummy_client = client.Client(self.instance, MockNull(), env,
4784-
[], None)
4814+
cgi.FieldStorage(), None)
47854815
self.dummy_client.db = self.db
47864816
self.dummy_client.request.headers.get = self.get_header
47874817
self.empty_form = cgi.FieldStorage()
@@ -4849,7 +4879,7 @@ def wh(s):
48494879
"REQUEST_METHOD": "GET"
48504880
}
48514881
self.dummy_client = client.Client(self.instance, MockNull(), env,
4852-
[], None)
4882+
cgi.FieldStorage(), None)
48534883
self.dummy_client.db = self.db
48544884
self.dummy_client.request.headers.get = self.get_header
48554885
self.empty_form = cgi.FieldStorage()
@@ -4891,7 +4921,7 @@ def wh(s):
48914921
"REQUEST_METHOD": "GET"
48924922
}
48934923
self.dummy_client = client.Client(self.instance, MockNull(), env,
4894-
[], None)
4924+
cgi.FieldStorage(), None)
48954925
self.dummy_client.db = self.db
48964926
self.dummy_client.request.headers.get = self.get_header
48974927
self.empty_form = cgi.FieldStorage()
@@ -4930,7 +4960,7 @@ def wh(s):
49304960
"REQUEST_METHOD": "GET"
49314961
}
49324962
self.dummy_client = client.Client(self.instance, MockNull(), env,
4933-
[], None)
4963+
cgi.FieldStorage(), None)
49344964
self.dummy_client.db = self.db
49354965
self.dummy_client.request.headers.get = self.get_header
49364966
self.empty_form = cgi.FieldStorage()
@@ -4966,7 +4996,7 @@ def wh(s):
49664996
"REQUEST_METHOD": "GET"
49674997
}
49684998
self.dummy_client = client.Client(self.instance, MockNull(), env,
4969-
[], None)
4999+
cgi.FieldStorage(), None)
49705000
self.dummy_client.db = self.db
49715001
self.dummy_client.request.headers.get = self.get_header
49725002
self.empty_form = cgi.FieldStorage()
@@ -5003,7 +5033,7 @@ def wh(s):
50035033
"REQUEST_METHOD": "GET"
50045034
}
50055035
self.dummy_client = client.Client(self.instance, MockNull(), env,
5006-
[], None)
5036+
cgi.FieldStorage(), None)
50075037
self.dummy_client.db = self.db
50085038
self.dummy_client.request.headers.get = self.get_header
50095039
self.empty_form = cgi.FieldStorage()
@@ -5039,7 +5069,7 @@ def wh(s):
50395069
"REQUEST_METHOD": "GET"
50405070
}
50415071
self.dummy_client = client.Client(self.instance, MockNull(), env,
5042-
[], None)
5072+
cgi.FieldStorage(), None)
50435073
self.dummy_client.db = self.db
50445074
self.dummy_client.request.headers.get = self.get_header
50455075
self.empty_form = cgi.FieldStorage()

test/test_cgi.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,8 +1870,11 @@ def testXmlrpcCsrfProtection(self):
18701870
def wh(s):
18711871
out.append(s)
18721872

1873-
# xmlrpc has no form content
1874-
form = {}
1873+
# create form for xmlrpc from string
1874+
form = db_test_base.makeFormFromString('xyzzy',
1875+
{"REQUEST_METHOD": "POST",
1876+
"CONTENT_TYPE": "text/json"})
1877+
18751878
cl = client.Client(self.instance, None,
18761879
{'REQUEST_METHOD':'POST',
18771880
'PATH_INFO':'xmlrpc',

test/test_liveserver.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,18 @@ def test_cookie_attributes(self):
333333
self.assertEqual(cookie._rest['HttpOnly'], None) # flag is present
334334
self.assertEqual(cookie._rest['SameSite'], 'Lax')
335335

336+
def test_bad_post_data(self):
337+
"""issue2551387 - bad post data causes TypeError: not indexable
338+
"""
339+
session, _response = self.create_login_session()
340+
341+
h = {"Content-Type": "text/plain"}
342+
response = session.post(self.url_base()+'/', headers=h, data="test")
343+
print(response.status_code)
344+
print(response.headers)
345+
print(response.text)
346+
self.assertEqual(response.status_code, 200)
347+
336348
def test_query(self):
337349
current_user_query = (
338350
"@columns=title,id,activity,status,assignedto&"

0 commit comments

Comments
 (0)