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 <[email protected]>
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