diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index 76f543bdb81..cd10ba64f3d 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -5,6 +5,7 @@ Changelog ------------------------ - Set seen_tours for all users in test fixture. [njohner] +- XSS: Escape html for plone formwidget autocomplete widget. [elioschmutz] - XSS: Escape html for proposal history entries. [elioschmutz] diff --git a/opengever/base/browser/configure.zcml b/opengever/base/browser/configure.zcml index e2651dfee7d..4eb30d5d68f 100644 --- a/opengever/base/browser/configure.zcml +++ b/opengever/base/browser/configure.zcml @@ -270,4 +270,12 @@ permission="zope2.View" /> + + diff --git a/opengever/base/browser/widgets.py b/opengever/base/browser/widgets.py new file mode 100644 index 00000000000..75e236b7ca7 --- /dev/null +++ b/opengever/base/browser/widgets.py @@ -0,0 +1,9 @@ +from plone.formwidget.autocomplete.widget import AutocompleteSearch +from opengever.base.utils import escape_html + + +class GeverAutocompleteSearch(AutocompleteSearch): + + def __call__(self): + result = super(GeverAutocompleteSearch, self).__call__() + return escape_html(result) diff --git a/opengever/base/tests/test_widget.py b/opengever/base/tests/test_widget.py index 20d98eb1f52..3932fc5569e 100644 --- a/opengever/base/tests/test_widget.py +++ b/opengever/base/tests/test_widget.py @@ -125,3 +125,24 @@ def test_renders_empty_message(self, browser): "No items available", browser.css('#formfield-form-widgets-empty_radio_table_field .empty_message').first.text ) + + +class TestGeverAutocompleteSearch(IntegrationTestCase): + @browsing + def test_autocomplete_result_is_escaped(self, browser): + self.login(self.manager, browser=browser) + self.subdossier.title = 'Evil ' + self.subdossier.reindexObject() + + browser.open(self.dossier, view="@@move_items/++widget++form.widgets.destination_folder/@@autocomplete-search?q=Evil") + + # The @@autocomplete-search view returns a string with `path|title\npath|title...` + items = browser.contents.split('\n') + + self.assertEqual( + 1, len(items), + "Due to the search query, only one dossier should be returned") + + path, title = items[0].split('|') + self.assertEqual('/'.join(self.subdossier.getPhysicalPath()), path) + self.assertEqual('Evil <script></script>', title, 'The item')