Skip to content

Commit 7ca348d

Browse files
committed
Different approach to fix XSS in issue2550817
Encapsulate the error/ok message append method as add_ok_message and add_error_message. The new approach escapes the messages when appending -- at a point in the code where we still know where the message comes from. Escaping is the default but can bei turned off. This also fixes issue2550836 where certain messages may contain links. Another advantage of the new fix is that users don't need to change installed trackers and are secure by default.
1 parent 28865cd commit 7ca348d

File tree

17 files changed

+191
-160
lines changed

17 files changed

+191
-160
lines changed

CHANGES.txt

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ Entries without name were done by Richard Jones.
1111

1212
Pay attention:
1313

14-
This release includes *important change affecting security*. Since
15-
this version escaping now happens in the template and not in the
16-
roundup code. Please read doc/upgrading.txt on how to change your
17-
templates. Without this you are vulnerable. (Ralf Schlatterbeck)
14+
If you have installed an intermediate version from our version control
15+
system and have modified your tracker instance to escape OK and
16+
error-messages in the HTML templates you need to revert this change.
17+
If you're upgrading from a previous roundup release version
18+
you should look into ``doc/upgrading.txt``. (Ralf Schlatterbeck)
1819

1920
Features:
2021

@@ -72,18 +73,18 @@ Fixed:
7273
JOIN clause was missing in generated SQL. (Ralf Schlatterbeck)
7374
- Fix another XSS issue2550817. Note that the code that triggers that
7475
particular bug is no longer in roundup core. But the change to the
75-
templates we suggest is a *lot* safer as it always escapes the error
76-
and ok messages now. Thanks to Thibault Fevry for the original
76+
templates we suggest is a *lot* safer as it by default escapes the
77+
error and ok messages now. Thanks to Thibault Fevry for the original
7778
bug-report.
7879
- issue2117897: Fixed two more places in date.py where seconds can be
7980
rounded to 60.0 and causing exceptions. Change them to 59.999 as was
8081
done in the fix for issue2550802. (Thomas Arendsen Hein)
8182
- Fix batch.propchanged for transitive id properties (would result in a
8283
backtrace when trying to group by property.id)
83-
- Fix issue2550835 which tests for date-range queries with an interval
84-
that depends on the local time. Put the queried date a little later to
85-
avoid a race condition where the queried interval doesn't match the
86-
date because the clock has advanced. (Ralf Schlatterbeck)
84+
- Fix issue2550835, the test checks for date-range queries with an
85+
interval that depends on the local time. Put the queried date a little
86+
later to avoid a race condition where the queried interval doesn't
87+
match the date because the clock has advanced. (Ralf Schlatterbeck)
8788

8889
Minor:
8990
- demo.py usage message improved: explains "nuke" now. (Bernhard Reiter)
@@ -335,10 +336,10 @@ Fixed:
335336
issue2550689, but is untested if this really works in browsers.
336337
Thanks to Joseph Myers for reporting. (Ralf)
337338
- Fix another XSS with the ok- and error message, see issue2550724. We
338-
solve this differently from the proposals in the bug-report by not
339-
allowing *any* html-tags in ok/error messages anymore. Thanks to
340-
David Benjamin for the bug-report and to Ezio Melotti for several
341-
proposed fixes. (Ralf)
339+
now escape messages when added to the list so we can decide whether to
340+
escape a message individually for each message. The default is to
341+
escape. Thanks to David Benjamin for the bug-report and to Ezio
342+
Melotti for several proposed fixes. (Ralf)
342343

343344

344345
2011-07-15: 1.4.19

doc/customizing.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2958,7 +2958,9 @@ See the docstring of that class for details of what it can do.
29582958
The method will typically check the ``self.form`` variable's contents.
29592959
It may then:
29602960

2961-
- add information to ``self.client.ok_message`` or ``self.client.error_message``
2961+
- add information to ``self.client._ok_message``
2962+
or ``self.client._error_message`` (by using ``self.client.add_ok_message``
2963+
or ``self.client.add_error_message``, respectively)
29622964
- change the ``self.client.template`` variable to alter what the user will see
29632965
next
29642966
- raise Unauthorised, SendStaticFile, SendFile, NotFound or Redirect
@@ -4993,7 +4995,7 @@ Setting up a "wizard" (or "druid") for controlled adding of issues
49934995
'''
49944996
category = self.form['category'].value
49954997
if category == '-1':
4996-
self.client.error_message.append('You must select a category of report')
4998+
self.client.add_error_message('You must select a category of report')
49974999
return
49985000
# everything's ok, move on to the next page
49995001
self.client.template = 'add_page2'

doc/upgrading.txt

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,27 @@ steps.
1616
Migrating from 1.5.0 to 1.5.1
1717
=============================
1818

19-
*Important*:
20-
There was a security bug fixed in the html templates (an XSS
21-
vulnerability). So if you have a running tracker you will have to fix
22-
the file ``html/page.html`` in your tracker directory. You need to
23-
*twice* remove the ``structure`` element in the template and modify the
24-
'tal:content' attribute, you need to replace the section::
25-
26-
<td>
27-
<p tal:condition="options/error_message | nothing" class="error-message"
28-
tal:repeat="m options/error_message"
29-
tal:content="structure string:$m <br/ > " />
30-
<p tal:condition="options/ok_message | nothing" class="ok-message">
31-
<span tal:repeat="m options/ok_message"
32-
tal:content="structure string:$m <br/ > " />
33-
<a class="form-small" tal:attributes="href request/current_url"
34-
i18n:translate="">clear this message</a>
35-
</p>
36-
</td>
37-
38-
with::
39-
40-
<td>
41-
<p tal:condition="options/error_message | nothing" class="error-message"
42-
tal:repeat="m options/error_message" tal:content="m" />
43-
<p tal:condition="options/ok_message | nothing" class="ok-message">
44-
<span tal:repeat="m options/ok_message" tal:content="m" />
45-
<a class="form-small" tal:attributes="href request/current_url"
46-
i18n:translate="">clear this message</a>
47-
</p>
48-
</td>
49-
50-
if you are using the new *jinja2* base templates, we are now iterating
51-
over the error- and ok-messages and creating a paragraph for each
52-
message. In addition ``autoescape`` is turned on for the section (which
53-
is the critical security change).
54-
See ``templates/jinja2/html/layout/page.html`` for details.
19+
If you have defined your own cgi actions in your tracker instance
20+
(e.g. in a custom ``extensions/spambayes.py`` file) you need to modify
21+
all cases where client.error_message or client.ok_message are modified
22+
directly. Instead of::
5523

24+
self.client.ok_message.append(...)
25+
26+
you need to call::
27+
28+
self.client.add_ok_message(...)
29+
30+
and the same for::
31+
32+
self.client.error_message.append(...)
33+
34+
vs.::
35+
36+
self.client.add_error_message(...)
37+
38+
The new calls escape the passed string by default and avoid XSS security
39+
issues.
5640

5741
Migrating from 1.4.20 to 1.4.21
5842
===============================

roundup/cgi/actions.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def handle(self):
131131
self.db.getclass(self.classname).retire(itemid)
132132
self.db.commit()
133133

134-
self.client.ok_message.append(
134+
self.client.add_ok_message(
135135
self._('%(classname)s %(itemid)s has been retired')%{
136136
'classname': self.classname.capitalize(), 'itemid': itemid})
137137

@@ -331,7 +331,7 @@ def handle(self):
331331

332332
# confirm correct weight
333333
if len(props_without_id) != len(values):
334-
self.client.error_message.append(
334+
self.client.add_error_message(
335335
self._('Not enough values on line %(line)s')%{'line':line})
336336
return
337337

@@ -392,7 +392,7 @@ def handle(self):
392392
# all OK
393393
self.db.commit()
394394

395-
self.client.ok_message.append(self._('Items edited OK'))
395+
self.client.add_ok_message(self._('Items edited OK'))
396396

397397
class EditCommon(Action):
398398
'''Utility methods for editing.'''
@@ -593,7 +593,7 @@ def handleCollision(self, props):
593593
'View <a target="new" href="%s%s">their changes</a> '
594594
'in a new window.')%(self.classname, ', '.join(props),
595595
self.classname, self.nodeid)
596-
self.client.error_message.append(message)
596+
self.client.add_error_message(message, escape=False)
597597
return
598598

599599
def handle(self):
@@ -620,7 +620,7 @@ def handle(self):
620620
message = self._editnodes(props, links)
621621
except (ValueError, KeyError, IndexError,
622622
roundup.exceptions.Reject), message:
623-
self.client.error_message.append(
623+
self.client.add_error_message(
624624
self._('Edit Error: %s') % str(message))
625625
return
626626

@@ -656,7 +656,7 @@ def handle(self):
656656
try:
657657
props, links = self.client.parsePropsFromForm(create=1)
658658
except (ValueError, KeyError), message:
659-
self.client.error_message.append(self._('Error: %s')
659+
self.client.add_error_message(self._('Error: %s')
660660
% str(message))
661661
return
662662

@@ -667,7 +667,7 @@ def handle(self):
667667
except (ValueError, KeyError, IndexError,
668668
roundup.exceptions.Reject), message:
669669
# these errors might just be indicative of user dumbness
670-
self.client.error_message.append(_('Error: %s') % str(message))
670+
self.client.add_error_message(_('Error: %s') % str(message))
671671
return
672672

673673
# commit now that all the tricky stuff is done
@@ -692,7 +692,7 @@ def handle(self):
692692
otk = self.form['otk'].value
693693
uid = otks.get(otk, 'uid', default=None)
694694
if uid is None:
695-
self.client.error_message.append(
695+
self.client.add_error_message(
696696
self._("Invalid One Time Key!\n"
697697
"(a Mozilla bug may cause this message "
698698
"to show up erroneously, please check your email)"))
@@ -716,7 +716,7 @@ def handle(self):
716716
otks.destroy(otk)
717717
self.db.commit()
718718
except (ValueError, KeyError), message:
719-
self.client.error_message.append(str(message))
719+
self.client.add_error_message(str(message))
720720
return
721721

722722
# user info
@@ -734,7 +734,7 @@ def handle(self):
734734
if not self.client.standard_message([address], subject, body):
735735
return
736736

737-
self.client.ok_message.append(
737+
self.client.add_ok_message(
738738
self._('Password reset and email sent to %s') % address)
739739
return
740740

@@ -744,19 +744,19 @@ def handle(self):
744744
try:
745745
uid = self.db.user.lookup(name)
746746
except KeyError:
747-
self.client.error_message.append(self._('Unknown username'))
747+
self.client.add_error_message(self._('Unknown username'))
748748
return
749749
address = self.db.user.get(uid, 'address')
750750
elif 'address' in self.form:
751751
address = self.form['address'].value
752752
uid = uidFromAddress(self.db, ('', address), create=0)
753753
if not uid:
754-
self.client.error_message.append(
754+
self.client.add_error_message(
755755
self._('Unknown email address'))
756756
return
757757
name = self.db.user.get(uid, 'username')
758758
else:
759-
self.client.error_message.append(
759+
self.client.add_error_message(
760760
self._('You need to specify a username or address'))
761761
return
762762

@@ -782,7 +782,7 @@ def handle(self):
782782
if not self.client.standard_message([address], subject, body):
783783
return
784784

785-
self.client.ok_message.append(self._('Email sent to %s') % address)
785+
self.client.add_ok_message(self._('Email sent to %s') % address)
786786

787787
class RegoCommon(Action):
788788
def finishRego(self):
@@ -815,7 +815,7 @@ def handle(self):
815815
# pull the rego information out of the otk database
816816
self.userid = self.db.confirm_registration(self.form['otk'].value)
817817
except (ValueError, KeyError), message:
818-
self.client.error_message.append(str(message))
818+
self.client.add_error_message(str(message))
819819
return
820820
return self.finishRego()
821821

@@ -837,7 +837,7 @@ def handle(self):
837837
try:
838838
props, links = self.client.parsePropsFromForm(create=1)
839839
except (ValueError, KeyError), message:
840-
self.client.error_message.append(self._('Error: %s')
840+
self.client.add_error_message(self._('Error: %s')
841841
% str(message))
842842
return
843843

@@ -850,7 +850,7 @@ def handle(self):
850850
except (ValueError, KeyError, IndexError,
851851
roundup.exceptions.Reject), message:
852852
# these errors might just be indicative of user dumbness
853-
self.client.error_message.append(_('Error: %s') % str(message))
853+
self.client.add_error_message(_('Error: %s') % str(message))
854854
return
855855

856856
# fix up the initial roles
@@ -938,7 +938,7 @@ def handle(self):
938938
self.client.session_api.destroy()
939939

940940
# Let the user know what's going on
941-
self.client.ok_message.append(self._('You are logged out'))
941+
self.client.add_ok_message(self._('You are logged out'))
942942

943943
# reset client context to render tracker home page
944944
# instead of last viewed page (may be inaccessibe for anonymous)
@@ -959,7 +959,7 @@ def handle(self):
959959

960960
# we need the username at a minimum
961961
if '__login_name' not in self.form:
962-
self.client.error_message.append(self._('Username required'))
962+
self.client.add_error_message(self._('Username required'))
963963
return
964964

965965
# get the login info
@@ -973,7 +973,8 @@ def handle(self):
973973
self.verifyLogin(self.client.user, password)
974974
except exceptions.LoginError, err:
975975
self.client.make_user_anonymous()
976-
self.client.error_message.extend(list(err.args))
976+
for arg in err.args:
977+
self.client.add_error_message(arg)
977978
return
978979

979980
# now we're OK, re-open the database for real, using the user

0 commit comments

Comments
 (0)