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')