You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by gc...@apache.org on 2026/09/11 16:10:42 UTC
(allura) 03/03: [#8623] bound the easywidgets resource cache and cap _slim requests
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch gc/8623
in repository https://gitbox.apache.org/repos/asf/allura.git
commit f6b0f4f65cfec1a7a87a7c1dae92ee41ef5a5056
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Fri Sep 11 16:08:03 2026 +0000
[#8623] bound the easywidgets resource cache and cap _slim requests
---
Allura/allura/config/middleware.py | 47 ++++++++++++++++++++++++++++++++++
Allura/allura/lib/custom_middleware.py | 31 ++++++++++++++++++++++
2 files changed, 78 insertions(+)
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 9914e8856..1bdd9d1b5 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -21,6 +21,7 @@
import importlib
import mimetypes
import re
+import threading
import warnings
from tg import config
@@ -33,6 +34,7 @@
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
@@ -55,6 +57,7 @@
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
@@ -69,6 +72,44 @@
__all__ = ['make_app']
+class _BoundedResourceCache(dict):
+ """A byte-budgeted replacement for easywidgets' unbounded resource cache.
+
+ ew 0.4.x keeps ResourceManager.resource_cache -- a plain class-level dict shared by every
+ instance and every request -- forever, keyed on the raw href query string and with no eviction.
+ WidgetMiddleware answers /_ew_resources/ before the wrapped app, so anyone at all can grow the
+ worker's heap until it dies. It only ever does ``cache[href]`` and ``cache[href] = content``,
+ so evicting on insert is enough to bound it without touching the library's code.
+
+ Oldest-first rather than true LRU: eviction only happens under abuse, where the recency of what
+ gets dropped does not matter. easywidgets 0.5 bounds this itself; drop once the pin moves.
+ """
+
+ 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.
@@ -173,6 +214,7 @@ 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,
@@ -200,6 +242,11 @@ 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 183356f35..5c8f427cc 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -18,6 +18,7 @@
import os
import re
import logging
+from urllib.parse import parse_qs
import tg
from paste import fileapp
@@ -657,3 +658,33 @@ 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)