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