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