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

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


The following commit(s) were added to refs/heads/master by this push:
     new 299dc4070 [#8470] added default csp headers and configurable options 
to add additional frame-src and form-action
299dc4070 is described below

commit 299dc407080bb7dcc6e4aa02eab0bc75cfd11677
Author: Guillermo Cruz <guillermo.c...@slashdotmedia.com>
AuthorDate: Wed Sep 28 11:21:10 2022 -0600

    [#8470] added default csp headers and configurable options to add 
additional frame-src and form-action
---
 Allura/allura/config/middleware.py          |  4 ++-
 Allura/allura/lib/app_globals.py            | 22 +++++++++++++
 Allura/allura/lib/custom_middleware.py      | 49 +++++++++++++++++++++++++++++
 Allura/allura/tests/functional/test_root.py | 25 +++++++++++++++
 Allura/development.ini                      | 13 ++++++++
 5 files changed, 112 insertions(+), 1 deletion(-)

diff --git a/Allura/allura/config/middleware.py 
b/Allura/allura/config/middleware.py
index e5ace854a..2d759c4d8 100644
--- a/Allura/allura/config/middleware.py
+++ b/Allura/allura/config/middleware.py
@@ -63,6 +63,7 @@ from allura.lib.custom_middleware import 
LoginRedirectMiddleware
 from allura.lib.custom_middleware import RememberLoginMiddleware
 from allura.lib.custom_middleware import SetRequestHostFromConfig
 from allura.lib.custom_middleware import MingTaskSessionSetupMiddleware
+from allura.lib.custom_middleware import ContentSecurityPolicyMiddleware
 from allura.lib import helpers as h
 from allura.lib.utils import configure_ming
 
@@ -142,7 +143,8 @@ def _make_core_app(root, global_conf, full_stack=True, 
**app_conf):
         Middleware = mw_ep.load()
         if getattr(Middleware, 'when', 'inner') == 'inner':
             app = Middleware(app, config)
-
+    # CSP headers
+    app = ContentSecurityPolicyMiddleware(app, config)
     # Required for sessions
     app = SessionMiddleware(app, config, 
data_serializer=BeakerPickleSerializerWithLatin1())
     # Handle "Remember me" functionality
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 65279bbca..5895459e4 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -662,6 +662,28 @@ class Globals:
     def commit_statuses_enabled(self):
         return asbool(config['scm.commit_statuses'])
 
+    @property
+    def csp_report_mode(self):
+        if config.get('csp.report_mode'):
+            return asbool(config['csp.report_mode'])
+        return False
+
+    @property
+    def csp_report_uri(self):
+        if config.get('csp.report_uri'):
+            return config['csp.report_uri']
+        return None
+    @property
+    def csp_report_uri_enforce(self):
+        if config.get('csp.report_uri_enforce'):
+            return config['csp.report_uri_enforce']
+        return None
+    @property
+    def csp_report_enforce(self):
+        if config.get('csp.report_enforce_mode'):
+            return True
+        return False
+
 class Icon:
 
     def __init__(self, css, title=None):
diff --git a/Allura/allura/lib/custom_middleware.py 
b/Allura/allura/lib/custom_middleware.py
index ca97111f0..7665f64de 100644
--- a/Allura/allura/lib/custom_middleware.py
+++ b/Allura/allura/lib/custom_middleware.py
@@ -35,6 +35,7 @@ from allura.lib import helpers as h
 from allura.lib.utils import is_ajax
 from allura import model as M
 import allura.model.repository
+from tg import tmpl_context as c, app_globals as g
 
 log = logging.getLogger(__name__)
 
@@ -455,3 +456,51 @@ class MingTaskSessionSetupMiddleware:
         # this is sufficient to ensure an ODM session is always established
         session(M.MonQTask).impl
         return self.app(environ, start_response)
+
+
+class ContentSecurityPolicyMiddleware:
+    ''' Sets Content-Security-Policy headers '''
+
+    def __init__(self, app, config):
+        self.app = app
+        self.config = config
+
+    def __call__(self, environ, start_response):
+        req = Request(environ)
+        resp = req.get_response(self.app)
+        rules = resp.headers.getall('Content-Security-Policy')
+        report_rules = 
resp.headers.getall('Content-Security-Policy-Report-Only')
+
+        if rules:
+            resp.headers.pop('Content-Security-Policy')
+        if report_rules:
+           resp.headers.pop('Content-Security-Policy-Report-Only')
+
+        if g.csp_report_mode and g.csp_report_uri:
+            report_rules.append(f'report-uri {g.csp_report_uri}; report-to 
{g.csp_report_uri}')
+
+        if self.config['base_url'].startswith('https'):
+            rules.append('upgrade-insecure-requests')
+
+        if g.csp_report_enforce and g.csp_report_uri_enforce:
+            rules.append(f'report-uri {g.csp_report_uri_enforce}; report-to 
{g.csp_report_uri_enforce:}')
+
+        if self.config.get('csp.frame_sources'):
+            if g.csp_report_mode:
+                report_rules.append(f"frame-src 
{self.config['csp.frame_sources']}")
+            else:
+                rules.append(f"frame-src {self.config['csp.frame_sources']}")
+
+        if self.config.get('csp.form_action_urls'):
+            if g.csp_report_mode:
+                report_rules.append(f"form-action 
{self.config['csp.form_action_urls']}")
+            else:
+                rules.append(f"form-action 
{self.config['csp.form_action_urls']}")
+
+        rules.append("object-src 'none'")
+        rules.append("frame-ancestors 'self'")
+        if rules:
+            resp.headers.add('Content-Security-Policy', '; '.join(rules))
+        if report_rules:
+            resp.headers.add('Content-Security-Policy-Report-Only', '; 
'.join(report_rules))
+        return resp(environ, start_response)
diff --git a/Allura/allura/tests/functional/test_root.py 
b/Allura/allura/tests/functional/test_root.py
index 83eadf9c5..6964fa0d5 100644
--- a/Allura/allura/tests/functional/test_root.py
+++ b/Allura/allura/tests/functional/test_root.py
@@ -35,6 +35,7 @@ from tg import tmpl_context as c
 from alluratest.tools import assert_equal, module_not_available
 from ming.orm.ormsession import ThreadLocalORMSession
 import mock
+import tg
 
 from allura.tests import decorators as td
 from allura.tests import TestController
@@ -187,6 +188,30 @@ class TestRootController(TestController):
         r = self.app.get('/error/document')
         r.mustcontain("We're sorry but we weren't able to process")
 
+    def test_headers(self):
+        resp = self.app.get('/p')
+        assert resp.headers.getall('Content-Security-Policy')[0] == '; 
'.join(["frame-src 'self' www.youtube-nocookie.com",
+                                                                  "form-action 
'self'",
+                                                                  "object-src 
'none'",
+                                                                  
"frame-ancestors 'self'"])
+
+    def test_headers_config(self):
+        resp = self.app.get('/p')
+        assert "frame-src 'self' www.youtube-nocookie.com;" in 
resp.headers.getall('Content-Security-Policy')[0]
+
+    @mock.patch.dict(tg.config, {'csp.report_mode': True, 'csp.report_uri': 
'https://example.com/r/d/csp/reportOnly'})
+    def test_headers_report(self):
+        resp = self.app.get('/p/wiki/Home/')
+        assert resp.headers.getall('Content-Security-Policy-Report-Only')[0] 
== '; '.join(["report-uri https://example.com/r/d/csp/reportOnly";,
+                                                                              
"report-to https://example.com/r/d/csp/reportOnly";,
+                                                                            
"frame-src 'self' www.youtube-nocookie.com",
+                                                                            
"form-action 'self'"])
+
+    @mock.patch.dict(tg.config, {'csp.report_uri_enforce': 
'https://example.com/r/d/csp/enforce', 'csp.report_enforce_mode': True})
+    def test_headers_report_enforce(self):
+        resp = self.app.get('/p/wiki/Home/')
+        assert "report-uri https://example.com/r/d/csp/enforce; report-to 
https://example.com/r/d/csp/enforce; frame-src 'self' 
www.youtube-nocookie.com;" in  resp.headers.getall('Content-Security-Policy')[0]
+
 
 class TestRootWithSSLPattern(TestController):
     def setUp(self):
diff --git a/Allura/development.ini b/Allura/development.ini
index 2a13baef9..3ded52b9b 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -658,6 +658,19 @@ userstats.count_lines_of_code = true
 ; Minutes to cache saved search "bins" numbers.  0 will disable entirely, so 
caches are permanent
 ;forgetracker.bin_cache_expire = 60
 
+; CSP Headers
+; enable report mode
+; csp.report_mode = false
+; csp.report_enforce_mode = false
+; csp.report_uri = https://example.com/r/d/csp/reportOnly
+; csp.report_uri_enforce = https://example.com/r/d/csp/enforce
+
+
+; frame-src list of valid sources for loading frames
+csp.frame_sources = 'self' www.youtube-nocookie.com
+
+; form-action valid sources that can be used as an HTML <form> action
+csp.form_action_urls = 'self'
 
 ;
 ; Settings for comment reactions

Reply via email to