You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2026/10/08 16:28:17 UTC

(allura) branch master updated (96e9d501d -> b97e6fb44)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git


    from 96e9d501d [#8629] updating dependencies
     new 1b3717eb4 Revert "[#8623] bound the easywidgets resource cache and cap _slim requests"
     new 2bcbaa9f4 Revert "[#8623] keep inline script/style bodies raw under easywidgets autoescaping"
     new b97e6fb44 [#8628] bumping EasyWidgets 0.4.3 -> 0.5.0

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Allura/allura/config/middleware.py                 | 52 +--------------
 Allura/allura/lib/custom_middleware.py             | 31 ---------
 Allura/allura/tests/test_middlewares.py            | 21 ++++--
 .../forgetracker/tests/functional/test_root.py     | 74 ++++++++++++++++++++++
 ForgeTracker/forgetracker/widgets/ticket_form.py   |  4 +-
 requirements.txt                                   |  2 +-
 6 files changed, 93 insertions(+), 91 deletions(-)


(allura) 02/03: Revert "[#8623] keep inline script/style bodies raw under easywidgets autoescaping"

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 2bcbaa9f44c8d4231256e24432a90070ccfbea29
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Fri Oct 2 19:04:45 2026 +0000

    Revert "[#8623] keep inline script/style bodies raw under easywidgets autoescaping"
    
    This reverts commit c30ee2a8c03c5ddd701db2a2d609fcd3a1323b2a.
---
 Allura/allura/config/middleware.py      | 17 ------------
 Allura/allura/tests/test_middlewares.py | 48 ---------------------------------
 2 files changed, 65 deletions(-)

diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 9914e8856..008a5338f 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -32,7 +32,6 @@
 
 import activitystream
 import ew
-import ew.jinja2_ew
 import formencode
 from ming.odm.middleware import MingMiddleware
 from beaker_session_jwt import JWTCookieSession
@@ -69,21 +68,6 @@
 __all__ = ['make_app']
 
 
-def _mark_ew_script_bodies_safe():
-    """Keep inline <script>/<style> bodies out of easywidgets' autoescaping.
-
-    easywidgets 0.4.x renders JSScript/CSSScript through a bare ``{{widget.text}}``.  That was
-    harmless while its jinja env had autoescaping off, but _make_core_app turns it on, which would
-    escape every inline script and style body in the site (``&&`` -> ``&amp;&amp;`` and so on).
-    Script bodies are developer-supplied markup by definition, so mark them safe.  0.5 does this
-    itself; drop this once requirements pin easywidgets >= 0.5.
-    """
-    ew.jinja2_ew.JSScript.WidgetClass.template = ew.jinja2_ew.Snippet(
-        '<script type="text/javascript">{{widget.text|safe}}</script>', 'jinja2')
-    ew.jinja2_ew.CSSScript.WidgetClass.template = ew.jinja2_ew.Snippet(
-        '<style>{{widget.text|safe}}</style>', 'jinja2')
-
-
 # this is webapp entry point from setup.py
 def make_app(global_conf: dict, **app_conf):
     override_root_module_name = app_conf.get('override_root', None)
@@ -172,7 +156,6 @@ def _make_core_app(root, global_conf: dict, **app_conf):
     if config.get('override_root') != 'task':
         app = SSLMiddleware(app, app_conf.get('no_redirect.pattern'),
                             app_conf.get('force_ssl.pattern'))
-    _mark_ew_script_bodies_safe()
     # Setup resource manager, widget context SOP
     app = ew.WidgetMiddleware(
         app,
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index b2e734f64..191e00377 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -16,15 +16,6 @@
 #       under the License.
 
 from mock import MagicMock, patch
-from paste.registry import Registry
-
-import ew.jinja2_ew
-from ew import widget_context
-from ew.core import WidgetContext
-from ew.render import TemplateEngine
-from ew.resource import ResourceManager
-
-from allura.config.middleware import _mark_ew_script_bodies_safe
 from allura.lib.custom_middleware import CORSMiddleware
 
 
@@ -112,42 +103,3 @@ def test_get_access_control_request_headers(self):
         assert f({key: ''}) == set()
         assert (f({key: 'Authorization, Accept'}) ==
                 {'authorization', 'accept'})
-
-
-class TestEwScriptBodiesStayRaw:
-    """_make_core_app turns easywidgets' autoescaping on, which would otherwise escape every
-    inline script and style body rendered through JSScript/CSSScript."""
-
-    def setup_method(self):
-        self.saved_engines = TemplateEngine._engines
-        self.saved_js = ew.jinja2_ew.JSScript.WidgetClass.template
-        self.saved_css = ew.jinja2_ew.CSSScript.WidgetClass.template
-        TemplateEngine.initialize({'jinja2.autoescape': True})
-        self.registry = Registry()
-        self.registry.prepare()
-        self.manager = ResourceManager()
-        self.registry.register(
-            widget_context, WidgetContext(scheme='http', resource_manager=self.manager))
-        _mark_ew_script_bodies_safe()
-
-    def teardown_method(self):
-        self.registry.cleanup()
-        TemplateEngine._engines = self.saved_engines
-        ew.jinja2_ew.JSScript.WidgetClass.template = self.saved_js
-        ew.jinja2_ew.CSSScript.WidgetClass.template = self.saved_css
-
-    def _render(self, widget):
-        widget.manager = self.manager
-        return widget.display()
-
-    def test_js_script_body_is_not_escaped(self):
-        body = '''$(function () {$('.x').go({"a": 1}); if (a && b) { c(); }});'''
-        assert body in self._render(ew.jinja2_ew.JSScript(body))
-
-    def test_css_script_body_is_not_escaped(self):
-        body = 'div.x table{ width: 700px; } a > b { color: red }'
-        assert body in self._render(ew.jinja2_ew.CSSScript(body))
-
-    def test_widget_values_are_still_escaped(self):
-        rendered = ew.jinja2_ew.TextField(name='f').display(value='a" onmouseover=alert(1)')
-        assert '&#34;' in rendered


(allura) 03/03: [#8628] bumping EasyWidgets 0.4.3 -> 0.5.0

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git

commit b97e6fb444121debd0d02cc3f8e8de23c425e9ab
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Mon Oct 5 15:28:19 2026 +0000

    [#8628] bumping EasyWidgets 0.4.3 -> 0.5.0
    
      * reverted security commits that are now included in the easywidgets itself
      * add some new easywidgets tests
---
 Allura/allura/config/middleware.py                 |  9 +--
 Allura/allura/lib/widgets/auth_widgets.py          | 12 ++--
 Allura/allura/lib/widgets/forms.py                 |  2 -
 Allura/allura/tests/test_middlewares.py            | 57 +++++++++++++++++
 .../forgetracker/tests/functional/test_root.py     | 74 ++++++++++++++++++++++
 requirements.txt                                   |  2 +-
 6 files changed, 138 insertions(+), 18 deletions(-)

diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 008a5338f..22c5d6f6b 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -166,16 +166,11 @@ def _make_core_app(root, global_conf: dict, **app_conf):
         extra_headers=ast.literal_eval(app_conf.get('ew.extra_headers', '[]')),
         cache_max_age=asint(app_conf.get('ew.cache_header_seconds', 60*60*24*365)),
 
-        # settings to pass through to jinja Environment for EW core widgets
+        # settings to pass through to a second jinja Environment for EW core widgets
         # these are for the easywidgets' own [easy_widgets.engines] entry point
         # (the Allura [easy_widgets.engines] entry point is named "jinja" (not jinja2) but it doesn't need
         #  any settings since it is a class that uses the same jinja env as the rest of allura)
-        # NB: this is a *second* jinja environment, separate from the one in
-        # AlluraJinjaRenderer.  Widgets render through this one and their output
-        # is spliced into our own templates as Markup, so it does not inherit
-        # their autoescaping -- it has to be turned on here too.  Recent
-        # easywidgets defaults it on; set it explicitly so an older pin can't
-        # silently render widgets unescaped.
+        # second jinja env; its output is spliced in as Markup, so it must autoescape itself.
         **{
             'jinja2.auto_reload': asbool(config['auto_reload_templates']),
             'jinja2.autoescape': True,
diff --git a/Allura/allura/lib/widgets/auth_widgets.py b/Allura/allura/lib/widgets/auth_widgets.py
index 38efc5df5..ef863229f 100644
--- a/Allura/allura/lib/widgets/auth_widgets.py
+++ b/Allura/allura/lib/widgets/auth_widgets.py
@@ -29,16 +29,12 @@
 from allura.lib import plugin
 
 
-# Emits already-rendered markup as-is.  The antispam honeypot fields arrive as
-# Markup, so passing them as *template source* is both pointless and risky --
-# a '$' in the content would be read as a core-ew substitution.  Render them as
-# data instead.
-_raw_html = ew.Snippet('{{widget.text|safe}}', 'jinja2')
-
-
 class LoginForm(ForgeForm):
     submit_text = 'Login'
     style = 'wide'
+    # The antispam honeypot fields arrive as Markup but it's better to
+    # render them as data.
+    _raw_html = ew.Snippet('{{widget.text|safe}}', 'jinja2')
 
     @property
     def fields(self):
@@ -69,7 +65,7 @@ def fields(self):
 
         for fld in g.antispam.extra_fields():
             fields.append(
-                ew_core.Widget(template=_raw_html, text=fld))
+                ew_core.Widget(template=self._raw_html, text=fld))
 
         return fields
 
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index de6eba00f..a3a3fce50 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -44,8 +44,6 @@
 
 
 class _HTMLExplanation(ew.InputField):
-    # text is developer-supplied markup (never user input); |safe keeps
-    # easywidgets' autoescaping from mangling it
     template = ew.Snippet(
         '''<label class="grid-4">&nbsp;</label>
            <div class="grid-14" style="margin:2px;">{{widget.text|safe}}</div>
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index 191e00377..af23cccf3 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -16,7 +16,18 @@
 #       under the License.
 
 from mock import MagicMock, patch
+from paste.registry import Registry
+
+import ew.jinja2_ew
+from ew import widget_context
+from ew.core import WidgetContext
+from ew.render import TemplateEngine
+from ew.resource import ResourceManager
+
+from allura.lib.widgets.forms import _HTMLExplanation
+
 from allura.lib.custom_middleware import CORSMiddleware
+from allura.tests import TestController
 
 
 class TestCORSMiddleware:
@@ -103,3 +114,49 @@ def test_get_access_control_request_headers(self):
         assert f({key: ''}) == set()
         assert (f({key: 'Authorization, Accept'}) ==
                 {'authorization', 'accept'})
+
+
+class TestEwScriptBodiesStayRaw:
+    """_make_core_app turns easywidgets' autoescaping on, which would otherwise escape every
+    inline script and style body rendered through JSScript/CSSScript."""
+
+    def setup_method(self):
+        self.saved_engines = TemplateEngine._engines
+        TemplateEngine.initialize({'jinja2.autoescape': True})
+        self.registry = Registry()
+        self.registry.prepare()
+        self.manager = ResourceManager()
+        self.registry.register(
+            widget_context, WidgetContext(scheme='http', resource_manager=self.manager))
+
+    def teardown_method(self):
+        self.registry.cleanup()
+        TemplateEngine._engines = self.saved_engines
+
+    def _render(self, widget):
+        widget.manager = self.manager
+        return widget.display()
+
+    def test_js_script_body_is_not_escaped(self):
+        body = '''$(function () {$('.x').go({"a": 1}); if (a && b) { c(); }});'''
+        assert body in self._render(ew.jinja2_ew.JSScript(body))
+
+    def test_css_script_body_is_not_escaped(self):
+        body = 'div.x table{ width: 700px; } a > b { color: red }'
+        assert body in self._render(ew.jinja2_ew.CSSScript(body))
+
+    def test_widget_values_are_still_escaped(self):
+        rendered = ew.jinja2_ew.TextField(name='f').display(value='a" onmouseover=alert(1)')
+        assert '&#34;' in rendered
+
+    def test_html_explanation_still_renders_its_markup(self):
+        """forms.py:46 relied on autoescape being off, so it needs an explicit |safe."""
+        rendered = _HTMLExplanation(text='<b>explanation</b>').display()
+        assert '<b>explanation</b>' in rendered
+
+
+class TestAutoescapeViaRealMiddleware(TestController):
+    """check on autoescape being set to on"""
+
+    def test_widget_middleware_autoescapes(self):
+        assert TemplateEngine.get_engine('jinja2')._environ.autoescape is True
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index a20649cc3..20d294669 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -90,6 +90,80 @@ def new_ticket(self, mount_point='/bugs/', extra_environ=None, **kw):
         return resp
 
 
+# Typed into a select field's "Options" box; shlex unescapes \" so a bare " reaches value="..."
+OPTIONS_PAYLOAD = r'"x\" onmouseover=alert(document.domain) autofocus onfocus=alert(1) y"'
+
+# Same breakout, closing the element instead of the attribute
+ELEMENT_PAYLOAD = r'"x\"><script>alert(document.domain)</script>"'
+
+# Milestone names only get .replace("/", "-"), so a quote reaches the same sink
+MILESTONE_PAYLOAD = r'1.0" onmouseover=alert(document.domain) x'
+
+FIELD_NAME = '_testselect'
+MILESTONE_FIELD_NAME = '_releases'
+
+
+def event_handler_attrs(tag):
+    """Every on*= attribute a browser would parse out of this subtree."""
+    found = set()
+    for el in [tag, *tag.find_all()]:
+        found.update(name for name in el.attrs if name.startswith('on'))
+    return found
+
+
+class TestSelectOptionsStoredXss(TrackerTestController):
+    """End-to-end test for stored XSS via select custom-field options: a tracker admin
+    POSTs the payload to the custom-fields admin endpoint and a different user's ticket
+    form renders it.  Library-level cases are unit-tested upstream, in easywidgets'
+    ew/tests/test_security.py.
+
+    Assertions are structural: the page may still contain the payload text inside an
+    attribute value, but must not contain an on* attribute or a <script> the browser
+    would parse out.
+    """
+
+    def _post_field(self, **field):
+        params = dict(
+            custom_fields=[field],
+            open_status_names='aa bb',
+            closed_status_names='cc',
+        )
+        self.app.post('/admin/bugs/set_custom_fields', params=variable_encode(params))
+
+    def _select_for(self, resp, field_name):
+        """The custom field's <select>, matched by name suffix (rendered names carry a form prefix)."""
+        selects = [s for s in resp.html.find_all('select')
+                   if (s.get('name') or '').endswith(field_name)]
+        assert len(selects) == 1
+        return selects[0]
+
+    @pytest.mark.parametrize('payload', [OPTIONS_PAYLOAD, ELEMENT_PAYLOAD])
+    def test_option_value_cannot_break_out_of_the_select(self, payload):
+        """Pre-fix, OPTIONS_PAYLOAD rendered onmouseover/autofocus/onfocus attributes and
+        ELEMENT_PAYLOAD closed the option and injected a <script> tag."""
+        self._post_field(name=FIELD_NAME, label='Test', type='select', options=payload)
+        select = self._select_for(self.app.get('/bugs/new/'), FIELD_NAME)
+        assert event_handler_attrs(select) == set()
+        assert select.find('script') is None
+
+    def test_field_label_is_not_a_second_hole(self):
+        """The label is escaped by ticket_custom_fields.html; moving it into the widget would reopen this."""
+        self._post_field(name=FIELD_NAME, label=r'<img src=x onerror=alert(document.domain)>',
+                          type='select', options='one two')
+        resp = self.app.get('/bugs/new/')
+        label = resp.html.find('label', attrs={'for': lambda v: bool(v) and v.endswith(FIELD_NAME)})
+        assert label is not None
+        assert label.find('img') is None
+
+    def test_milestone_name_cannot_break_out_of_the_value_attribute(self):
+        """MilestoneField has its own value= sink.  Names are escaped, not stripped, to round-trip."""
+        self._post_field(label='releases', show_in_search='on', type='milestone',
+                          milestones=[dict(name=MILESTONE_PAYLOAD)])
+        select = self._select_for(self.app.get('/bugs/new/'), MILESTONE_FIELD_NAME)
+        assert event_handler_attrs(select) == set()
+        assert '"' in select.find('option')['value']
+
+
 class TestMilestones(TrackerTestController):
     def test_milestone_list(self):
         r = self.app.get('/bugs/milestones')
diff --git a/requirements.txt b/requirements.txt
index bb9aa66cd..8842d0a0c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -72,7 +72,7 @@ docutils==0.23
     #   -r requirements.in
     #   defopt
     #   pypeline
-easywidgets==0.4.3
+easywidgets==0.5.0
     # via -r requirements.in
 emoji==2.15.0
     # via -r requirements.in


(allura) 01/03: Revert "[#8623] bound the easywidgets resource cache and cap _slim requests"

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 1b3717eb4adae99ea8effd84dff8d5ded5a531b3
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Fri Oct 2 19:04:45 2026 +0000

    Revert "[#8623] bound the easywidgets resource cache and cap _slim requests"
    
    This reverts commit ebb6c7c87af22db90671b2eefa206a66a55d0991.
---
 Allura/allura/config/middleware.py               | 54 ++++++------------------
 Allura/allura/lib/custom_middleware.py           | 31 --------------
 Allura/allura/lib/widgets/auth_widgets.py        | 12 ++++--
 Allura/allura/lib/widgets/forms.py               |  2 +
 ForgeTracker/forgetracker/widgets/ticket_form.py |  4 +-
 5 files changed, 25 insertions(+), 78 deletions(-)

diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 3e969e43d..9914e8856 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -21,7 +21,6 @@
 import importlib
 import mimetypes
 import re
-import threading
 import warnings
 
 from tg import config
@@ -34,7 +33,6 @@
 import activitystream
 import ew
 import ew.jinja2_ew
-import ew.resource
 import formencode
 from ming.odm.middleware import MingMiddleware
 from beaker_session_jwt import JWTCookieSession
@@ -57,7 +55,6 @@
 from allura.lib.custom_middleware import SSLMiddleware
 from allura.lib.custom_middleware import StaticFilesMiddleware
 from allura.lib.custom_middleware import CSRFMiddleware
-from allura.lib.custom_middleware import EwSlimRequestLimitMiddleware
 from allura.lib.custom_middleware import CORSMiddleware
 from allura.lib.custom_middleware import LoginRedirectMiddleware
 from allura.lib.custom_middleware import RememberLoginMiddleware
@@ -72,39 +69,14 @@
 __all__ = ['make_app']
 
 
-class _BoundedResourceCache(dict):
-    """A byte-budgeted replacement for easywidgets' unbounded resource cache.
-    drop this once requirements pin easywidgets >= 0.5.
-    """
-
-    def __init__(self, max_bytes):
-        super().__init__()
-        self.max_bytes = max_bytes
-        self._bytes = 0
-        self._lock = threading.Lock()
-
-    def __setitem__(self, key, value):
-        if len(value) > self.max_bytes:
-            return
-        with self._lock:
-            if key in self:
-                self._bytes -= len(super().__getitem__(key))
-                super().__delitem__(key)
-            super().__setitem__(key, value)
-            self._bytes += len(value)
-            while self._bytes > self.max_bytes and len(self) > 1:
-                oldest = next(iter(self))
-                self._bytes -= len(super().__getitem__(oldest))
-                super().__delitem__(oldest)
-
-
-def _bound_ew_resource_cache(max_bytes):
-    ew.resource.ResourceManager.resource_cache = _BoundedResourceCache(max_bytes)
-
-
 def _mark_ew_script_bodies_safe():
     """Keep inline <script>/<style> bodies out of easywidgets' autoescaping.
-    drop this once requirements pin easywidgets >= 0.5.
+
+    easywidgets 0.4.x renders JSScript/CSSScript through a bare ``{{widget.text}}``.  That was
+    harmless while its jinja env had autoescaping off, but _make_core_app turns it on, which would
+    escape every inline script and style body in the site (``&&`` -> ``&amp;&amp;`` and so on).
+    Script bodies are developer-supplied markup by definition, so mark them safe.  0.5 does this
+    itself; drop this once requirements pin easywidgets >= 0.5.
     """
     ew.jinja2_ew.JSScript.WidgetClass.template = ew.jinja2_ew.Snippet(
         '<script type="text/javascript">{{widget.text|safe}}</script>', 'jinja2')
@@ -201,7 +173,6 @@ def _make_core_app(root, global_conf: dict, **app_conf):
         app = SSLMiddleware(app, app_conf.get('no_redirect.pattern'),
                             app_conf.get('force_ssl.pattern'))
     _mark_ew_script_bodies_safe()
-    _bound_ew_resource_cache(asint(app_conf.get('ew.max_cache_bytes', 64 * 1024 * 1024)))
     # Setup resource manager, widget context SOP
     app = ew.WidgetMiddleware(
         app,
@@ -212,10 +183,16 @@ def _make_core_app(root, global_conf: dict, **app_conf):
         extra_headers=ast.literal_eval(app_conf.get('ew.extra_headers', '[]')),
         cache_max_age=asint(app_conf.get('ew.cache_header_seconds', 60*60*24*365)),
 
-        # settings to pass through to a second jinja Environment for EW core widgets
+        # settings to pass through to jinja Environment for EW core widgets
         # these are for the easywidgets' own [easy_widgets.engines] entry point
         # (the Allura [easy_widgets.engines] entry point is named "jinja" (not jinja2) but it doesn't need
         #  any settings since it is a class that uses the same jinja env as the rest of allura)
+        # NB: this is a *second* jinja environment, separate from the one in
+        # AlluraJinjaRenderer.  Widgets render through this one and their output
+        # is spliced into our own templates as Markup, so it does not inherit
+        # their autoescaping -- it has to be turned on here too.  Recent
+        # easywidgets defaults it on; set it explicitly so an older pin can't
+        # silently render widgets unescaped.
         **{
             'jinja2.auto_reload': asbool(config['auto_reload_templates']),
             'jinja2.autoescape': True,
@@ -223,11 +200,6 @@ def _make_core_app(root, global_conf: dict, **app_conf):
             'jinja2.cache_size': asint(config.get('jinja_cache_size', -1)),
         }
     )
-    # Refuse oversized _slim requests before WidgetMiddleware can concatenate them
-    app = EwSlimRequestLimitMiddleware(
-        app,
-        script_name=app_conf.get('ew.script_name', '/_ew_resources/'),
-        max_hrefs=asint(app_conf.get('ew.max_slim_hrefs', 100)))
     # Handle static files (by tool)
     app = StaticFilesMiddleware(app, app_conf.get('static.script_name'))
     # Handle setup and flushing of Ming ORM sessions
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index 5c8f427cc..183356f35 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -18,7 +18,6 @@
 import os
 import re
 import logging
-from urllib.parse import parse_qs
 
 import tg
 from paste import fileapp
@@ -658,33 +657,3 @@ def __call__(self, environ, start_response):
             newstatus, headers, app_iter, exc_info = _call_wsgi_application(self.app, new_environ)
         start_response(status, headers, exc_info)
         return app_iter
-
-
-class EwSlimRequestLimitMiddleware:
-    """Cap how many files one /_ew_resources/_slim/ request may concatenate.
-
-    easywidgets 0.4.x joins as many files as the query string names, with no bound, and its
-    WidgetMiddleware answers that path before the wrapped app sees it -- so an unauthenticated
-    request can name one file hundreds of times and get back a response hundreds of times its own
-    size, which is then cached for the life of the process.  Naming Allura's largest registered
-    resource 500 times turns a 9 KB query string into a 155 MB response.
-
-    This has to sit *outside* WidgetMiddleware so oversized requests never reach it.  easywidgets
-    0.5 enforces the same limit itself via max_slim_hrefs; drop this once the pin moves.
-    """
-
-    def __init__(self, app, script_name='/_ew_resources/', max_hrefs=100):
-        self.app = app
-        self.slim_prefix = script_name + '_slim/'
-        self.max_hrefs = max_hrefs
-
-    def __call__(self, environ, start_response):
-        if environ.get('PATH_INFO', '').startswith(self.slim_prefix):
-            href = parse_qs(environ.get('QUERY_STRING', '')).get('href', [''])[0]
-            count = len([h for h in href.split(';') if h])
-            if count > self.max_hrefs:
-                log.warning('Refusing _slim request for %s files (max %s) from %s',
-                            count, self.max_hrefs, environ.get('REMOTE_ADDR'))
-                resp = exc.HTTPBadRequest('Too many resources requested')
-                return resp(environ, start_response)
-        return self.app(environ, start_response)
diff --git a/Allura/allura/lib/widgets/auth_widgets.py b/Allura/allura/lib/widgets/auth_widgets.py
index ef863229f..38efc5df5 100644
--- a/Allura/allura/lib/widgets/auth_widgets.py
+++ b/Allura/allura/lib/widgets/auth_widgets.py
@@ -29,12 +29,16 @@
 from allura.lib import plugin
 
 
+# Emits already-rendered markup as-is.  The antispam honeypot fields arrive as
+# Markup, so passing them as *template source* is both pointless and risky --
+# a '$' in the content would be read as a core-ew substitution.  Render them as
+# data instead.
+_raw_html = ew.Snippet('{{widget.text|safe}}', 'jinja2')
+
+
 class LoginForm(ForgeForm):
     submit_text = 'Login'
     style = 'wide'
-    # The antispam honeypot fields arrive as Markup but it's better to
-    # render them as data.
-    _raw_html = ew.Snippet('{{widget.text|safe}}', 'jinja2')
 
     @property
     def fields(self):
@@ -65,7 +69,7 @@ def fields(self):
 
         for fld in g.antispam.extra_fields():
             fields.append(
-                ew_core.Widget(template=self._raw_html, text=fld))
+                ew_core.Widget(template=_raw_html, text=fld))
 
         return fields
 
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index a3a3fce50..de6eba00f 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -44,6 +44,8 @@
 
 
 class _HTMLExplanation(ew.InputField):
+    # text is developer-supplied markup (never user input); |safe keeps
+    # easywidgets' autoescaping from mangling it
     template = ew.Snippet(
         '''<label class="grid-4">&nbsp;</label>
            <div class="grid-14" style="margin:2px;">{{widget.text|safe}}</div>
diff --git a/ForgeTracker/forgetracker/widgets/ticket_form.py b/ForgeTracker/forgetracker/widgets/ticket_form.py
index 30b6f8fea..6a3e59368 100644
--- a/ForgeTracker/forgetracker/widgets/ticket_form.py
+++ b/ForgeTracker/forgetracker/widgets/ticket_form.py
@@ -240,12 +240,12 @@ class MilestoneField(ew.SingleSelectField):
                'class':css_class},
                attrs)}}>
             {% for o in open_milestones %}
-            <option{% if o.selected%} selected{% endif %} value="{{o.html_value|e}}">{{o.label}}</option>
+            <option{% if o.selected%} selected{% endif %} value="{{o.html_value|e}}">{{o.label|e}}</option>
             {% endfor %}
             {% if closed_milestones %}
             <optgroup label="Closed">
                 {% for o in closed_milestones %}
-                <option{% if o.selected%} selected{% endif %} value="{{o.html_value|e}}">{{o.label}}</option>
+                <option{% if o.selected%} selected{% endif %} value="{{o.html_value|e}}">{{o.label|e}}</option>
                 {% endfor %}
             </optgroup>
             {% endif %}