You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/10/27 18:56:50 UTC
(allura) 02/02: [#8633] reject /auth/ and /rest/ requests made as subresources (img, iframe, etc)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8633
in repository https://gitbox.apache.org/repos/asf/allura.git
commit a59754d6fecb0aaa15c3f3e4b5bf86b2de9e80be
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Mon Oct 26 17:16:43 2026 -0400
[#8633] reject /auth/ and /rest/ requests made as subresources (img, iframe, etc)
Uses Sec-Fetch-Dest so user content can't trigger these URLs with the viewer's session
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
Allura/allura/config/middleware.py | 3 ++-
Allura/allura/lib/custom_middleware.py | 19 +++++++++++++++++++
Allura/allura/tests/functional/test_admin.py | 6 +++---
Allura/allura/tests/functional/test_auth.py | 7 +++++++
Allura/allura/tests/test_middlewares.py | 15 +++++++++++++++
5 files changed, 46 insertions(+), 4 deletions(-)
diff --git a/Allura/allura/config/middleware.py b/Allura/allura/config/middleware.py
index 814501b2e..4309bfa2c 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -53,7 +53,7 @@
from allura.lib.custom_middleware import AlluraTimerMiddleware
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 CSRFMiddleware, SecFetchDestMiddleware
from allura.lib.custom_middleware import CORSMiddleware
from allura.lib.custom_middleware import LoginRedirectMiddleware
from allura.lib.custom_middleware import RememberLoginMiddleware
@@ -144,6 +144,7 @@ def _make_core_app(root, global_conf: dict, **app_conf):
# Clear cookies when the CSRF field isn't posted
if not app_conf.get('disable_csrf_protection'):
app = CSRFMiddleware(app, '_csrf_token')
+ app = SecFetchDestMiddleware(app)
if asbool(config.get('cors.enabled', False)):
# Handle CORS requests
allowed_methods = aslist(config.get('cors.methods'))
diff --git a/Allura/allura/lib/custom_middleware.py b/Allura/allura/lib/custom_middleware.py
index b64cd7873..2382b84e7 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -456,6 +456,25 @@ def __call__(self, environ, start_response):
return self.app(environ, start_response)
+class SecFetchDestMiddleware:
+ '''
+ Rejects /auth/ and /rest/ requests made as subresources (<img>, <iframe>, <script> etc), since those can be
+ embedded in user content and would carry the user's session. Navigations and fetch/XHR are allowed.
+ Browsers that don't send Sec-Fetch-Dest are allowed through.
+ '''
+ PATHS_ENFORCED = ('/auth/', '/rest/')
+ ALLOWED_VALS = ('document', 'empty', None)
+
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ dest = environ.get('HTTP_SEC_FETCH_DEST')
+ if dest not in self.ALLOWED_VALS and environ.get('PATH_INFO', '').startswith(self.PATHS_ENFORCED):
+ return exc.HTTPForbidden()(environ, start_response)
+ return self.app(environ, start_response)
+
+
class ContentSecurityPolicyMiddleware:
''' Sets Content-Security-Policy headers '''
diff --git a/Allura/allura/tests/functional/test_admin.py b/Allura/allura/tests/functional/test_admin.py
index 72228d036..1dce405b2 100644
--- a/Allura/allura/tests/functional/test_admin.py
+++ b/Allura/allura/tests/functional/test_admin.py
@@ -389,7 +389,7 @@ def test_project_icon(self):
image = PIL.Image.open(BytesIO(r.body))
assert image.size == (48, 48)
- r = self.app.get('/p/test/icon?foo=bar')
+ r = self.app.get('/p/test/icon?foo=bar', headers={'Sec-Fetch-Dest': 'image'})
r = self.app.get('/p/test/icon?w=96&w=100&w=10')
image = PIL.Image.open(BytesIO(r.body))
assert image.size == (96, 96)
@@ -414,11 +414,11 @@ def test_project_screenshot(self, uuid1):
project = M.Project.query.get(
shortname='test', neighborhood_id=p_nbhd._id)
filename = project.get_screenshots()[0].filename
- r = self.app.get('/p/test/screenshot/' + filename)
+ r = self.app.get('/p/test/screenshot/' + filename, headers={'Sec-Fetch-Dest': 'image'})
uploaded = PIL.Image.open(file_path)
screenshot = PIL.Image.open(BytesIO(r.body))
assert uploaded.size == screenshot.size
- r = self.app.get('/p/test/screenshot/' + filename + '/thumb')
+ r = self.app.get('/p/test/screenshot/' + filename + '/thumb', headers={'Sec-Fetch-Dest': 'image'})
thumb = PIL.Image.open(BytesIO(r.body))
assert thumb.size == (150, 150)
# FIX: home pages don't currently support screenshots (now that they're a wiki);
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index d6503eb91..5983dc86e 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -2385,6 +2385,13 @@ def test_authorize_invalid(self):
resp = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'}, status=400)
resp.mustcontain('error=invalid_client')
+ def test_authorize_subresource_rejected(self):
+ for dest in ('image', 'iframe'):
+ self.app.get('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'},
+ headers={'Sec-Fetch-Dest': dest}, status=403)
+ self.app.get('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'},
+ headers={'Sec-Fetch-Dest': 'document'}, status=400)
+
def test_do_authorize_no(self):
user = M.User.by_username('test-admin')
ctok = M.OAuthConsumerToken(
diff --git a/Allura/allura/tests/test_middlewares.py b/Allura/allura/tests/test_middlewares.py
index af23cccf3..18569872a 100644
--- a/Allura/allura/tests/test_middlewares.py
+++ b/Allura/allura/tests/test_middlewares.py
@@ -160,3 +160,18 @@ class TestAutoescapeViaRealMiddleware(TestController):
def test_widget_middleware_autoescapes(self):
assert TemplateEngine.get_engine('jinja2')._environ.autoescape is True
+
+
+class TestSecFetchDestMiddleware(TestController):
+
+ def test_subresource_blocked(self):
+ for path in ('/rest/foo/', '/auth/set_something_important'):
+ for dest in ('image', 'iframe', 'script'):
+ self.app.get(path, headers={'Sec-Fetch-Dest': dest}, status=403)
+
+ def test_allowed(self):
+ for dest in ('document', 'empty'):
+ self.app.get('/rest/', headers={'Sec-Fetch-Dest': dest})
+ self.app.get('/rest/')
+ self.app.get('/', headers={'Sec-Fetch-Dest': 'document'})
+