Skip to content

Commit 9d12b37

Browse files
author
Richard Jones
committed
backported XSS message cleaning fix [SF#757128]
1 parent 87dccd8 commit 9d12b37

File tree

3 files changed

+39
-9
lines changed

3 files changed

+39
-9
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
This file contains the changes to the Roundup system over time. The entries
22
are given with the most recent entry first.
33

4+
2003-07-?? 0.5.9
5+
- backported XSS message cleaning fix (sf bug 757128)
6+
47

58
2003-06-19 0.5.8
69
- plugged cross-site-scripting hole (thanks Jeff Epler)

roundup/cgi/client.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# $Id: client.py,v 1.65.2.9 2003-06-19 23:02:32 richard Exp $
1+
# $Id: client.py,v 1.65.2.10 2003-06-24 03:33:56 richard Exp $
22

33
__doc__ = """
44
WWW request handler (also used in the stand-alone server).
@@ -47,10 +47,16 @@ def initialiseSecurity(security):
4747
description="User may manipulate user Roles through the web")
4848
security.addPermissionToRole('Admin', p)
4949

50-
def clean_message(match, ok={'a':1,'i':1,'b':1,'br':1}):
50+
# used to clean messages passed through CGI variables - HTML-escape any tag
51+
# that isn't <a href="">, <i>, <b> and <br> (including XHTML variants) so
52+
# that people can't pass through nasties like <script>, <iframe>, ...
53+
CLEAN_MESSAGE_RE = r'(<(/?(.*?)(\s*href="[^"]")?\s*/?)>)'
54+
def clean_message(message, mc=re.compile(CLEAN_MESSAGE_RE, re.I)):
55+
return mc.sub(clean_message_callback, message)
56+
def clean_message_callback(match, ok={'a':1,'i':1,'b':1,'br':1}):
5157
''' Strip all non <a>,<i>,<b> and <br> tags from a string
5258
'''
53-
if ok.has_key(match.group(2)):
59+
if ok.has_key(match.group(3).lower()):
5460
return match.group(1)
5561
return '&lt;%s&gt;'%match.group(2)
5662

@@ -256,8 +262,7 @@ def determine_user(self):
256262
# reopen the database as the correct user
257263
self.opendb(self.user)
258264

259-
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)'),
260-
mc=re.compile(r'(</?(.*?)>)')):
265+
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
261266
''' Determine the context of this page from the URL:
262267
263268
The URL path after the instance identifier is examined. The path
@@ -339,10 +344,10 @@ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)'),
339344

340345
# see if we were passed in a message
341346
if self.form.has_key(':ok_message'):
342-
msg = mc.sub(clean_message, self.form[':ok_message'].value)
347+
msg = clean_message(self.form[':ok_message'].value)
343348
self.ok_message.append(msg)
344349
if self.form.has_key(':error_message'):
345-
msg = mc.sub(clean_message, self.form[':error_message'].value)
350+
msg = clean_message(self.form[':error_message'].value)
346351
self.error_message.append(msg)
347352

348353
def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):

test/test_cgi.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# but WITHOUT ANY WARRANTY; without even the implied warranty of
99
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1010
#
11-
# $Id: test_cgi.py,v 1.4.2.2 2003-03-21 21:43:04 richard Exp $
11+
# $Id: test_cgi.py,v 1.4.2.3 2003-06-24 03:33:56 richard Exp $
1212

1313
import unittest, os, shutil, errno, sys, difflib, cgi
1414

@@ -24,6 +24,26 @@ def makeForm(args):
2424
form.list.append(cgi.MiniFieldStorage(k, v))
2525
return form
2626

27+
cm = client.clean_message
28+
class MessageTestCase(unittest.TestCase):
29+
def testCleanMessageOK(self):
30+
self.assertEqual(cm('<br>x<br />'), '<br>x<br />')
31+
self.assertEqual(cm('<i>x</i>'), '<i>x</i>')
32+
self.assertEqual(cm('<b>x</b>'), '<b>x</b>')
33+
self.assertEqual(cm('<a href="y">x</a>'),
34+
'<a href="y">x</a>')
35+
self.assertEqual(cm('<BR>x<BR />'), '<BR>x<BR />')
36+
self.assertEqual(cm('<I>x</I>'), '<I>x</I>')
37+
self.assertEqual(cm('<B>x</B>'), '<B>x</B>')
38+
self.assertEqual(cm('<A HREF="y">x</A>'),
39+
'<A HREF="y">x</A>')
40+
41+
def testCleanMessageBAD(self):
42+
self.assertEqual(cm('<script>x</script>'),
43+
'&lt;script&gt;x&lt;/script&gt;')
44+
self.assertEqual(cm('<iframe>x</iframe>'),
45+
'&lt;iframe&gt;x&lt;/iframe&gt;')
46+
2747
class FormTestCase(unittest.TestCase):
2848
def setUp(self):
2949
self.dirname = '_test_cgi_form'
@@ -277,7 +297,9 @@ def testEmptyPasswordNOTSet(self):
277297

278298

279299
def suite():
280-
l = [unittest.makeSuite(FormTestCase),
300+
l = [
301+
unittest.makeSuite(FormTestCase),
302+
unittest.makeSuite(MessageTestCase),
281303
]
282304
return unittest.TestSuite(l)
283305

0 commit comments

Comments
 (0)