diff doc/admin_guide.txt @ 7238:98d7936d97a3

Add section on configuring Content Security Policy (CSP) Initial pass to add CSP. Also 2 typo fixes.
author John Rouillard <rouilj@ieee.org>
date Thu, 30 Mar 2023 19:37:48 -0400
parents f72ce883e677
children 16d6d81b4565
line wrap: on
line diff
--- a/doc/admin_guide.txt	Thu Mar 30 19:35:08 2023 -0400
+++ b/doc/admin_guide.txt	Thu Mar 30 19:37:48 2023 -0400
@@ -286,6 +286,173 @@
 mechanism allows the admin to allow use of brotli and zstd for
 dynamic content, but not for static content.
 
+Adding a Web Content Security Policy (CSP)
+==========================================
+
+A Content Security Policy (`CSP`_) adds a layer of security to
+Roundup's web interface. It makes it more difficult for an
+attacker to compromise Roundup.  By default Roundup does not add
+a CSP. If you need to implement a CSP, this section will help you
+understand how to add one and document the current level of
+support for CSP in Roundup.
+
+Roundup's web interface has remained mostly unchanged since it
+was created over a decade ago. Current releases have been slowly
+modernizing the HTML to improve security. There are still some
+improvements that need to happen before the tightest CSP
+configurations can be used.
+
+Writing a CSP is complex.  This section just touches on how to
+create and install a CSP to improve security. Some of it might
+break functionality.
+
+There are two ways to add a CSP:
+
+  1. a fixed CSP added by a server
+  2. a dynamic CSP added by Roundup
+
+Fixed CSP
+---------
+
+If you are using a web server (Apache, Nginx) to run Roundup, you can
+add a ``Content-Security-Policy`` header using that server. WSGI
+servers like uWSGI can also be configured to add headers.  An example
+header would look like::
+
+  Content-Security-Policy: default-src 'self' 'unsafe-inline' 'strict-dynamic';
+
+One thing that may need to be included is the ``unsafe-inline``.
+The default templates use ``onload``, ``onchange``, ``onsubmit``,
+and ``onclick`` JavaScript handlers. Without ``unsafe-inline``
+these won't work and popup helpers will not work. Sadly the use
+of ``unsafe-inline`` is a pretty big hole in this CSP.  You can
+set the hashes for all the JavaScript handlers in the CSP. Then
+replace ``unsafe-inline`` with ``unsafe-hashes`` to help close
+this hole, but has its own issues. See `remediating
+unsafe-inline`_ for another way to mitigate this.
+
+The inclusion of ``strict-dynamic`` allows trusted JavaScript
+files that are downloaded from Roundup to make changes to the web
+interface. These changes are also trusted code that will be run
+when invoked.
+
+More secure CSPs can also be created. However because of the ability
+to customize the web interface, it is difficult to provide guidance.
+
+Dynamic CSP
+-----------
+
+Roundup creates a cryptographic nonce for every client request. The
+nonce is the value of the ``client.client_nonce`` property.
+
+By changing the templates to use the nonce, we can better secure the 
+Roundup instance. However the nonce has to be set in the CSP returned
+by Roundup.
+
+One way to do this is to add a templating utility to the extensions
+directory that generates the CSP on the fly. For example::
+
+    default_security_headers = {
+	'Content-Security-Policy': (
+	    "default-src 'self'; "
+	    "base-uri 'self'; "
+	    "script-src https: 'nonce-{nonce}' 'strict-dynamic'; "
+	    "style-src 'self' 'nonce-{nonce}'; "
+	    "img-src 'self' data:; "
+	    "frame-ancestors 'self'; "
+	    "object-src 'self' 'nonce-{nonce}'; "
+	),
+    }
+
+
+    def AddHtmlHeaders(client, header_dict=None):
+	''' Generate https headers from dict use default security headers
+
+	    Setting the header with a value of None will not inject the
+	    header and can override the default set.
+
+	    Header values will be formatted with a dictionary including a
+	    nonce. Use to set a nonce for inline scripts.
+	'''
+	try:
+	    if client.client_nonce is None:
+		# logger.warning("client_nonce is None")
+		client.client_nonce = client.session_api._gen_sid()
+	except AttributeError:
+	    # client.client_nonce doesn't exist, create it
+	    # logger.warning("client_nonce does not exist, creating")
+	    client.client_nonce = client.session_api._gen_sid()
+
+	headers = default_security_headers.copy()
+	if isinstance(header_dict, dict):
+            headers.update(header_dict)
+
+	client_headers = client.additional_headers
+
+	for header, value in list(headers.items()):
+	    if value is None:
+		continue
+	    client_headers[header] = value.format(
+                nonce=client.client_nonce)
+
+    def init(instance):
+        instance.registerUtil('AddHtmlHeaders', AddHtmlHeaders)
+
+
+Adding the following to ``page.html`` right after the opening
+``<html....`>`` tag::
+
+   <tal:code tal:content="python:utils.AddHtmlHeaders(request.client)" />
+
+will invoke ``AddHtmlHeaders()`` to add the CSP header with the nonce.
+
+With this set of CSP headers, all style, script and object tags will
+need a ``nonce`` attribute. This can be added by changing::
+
+    <script src="javascript.js"></script>
+
+to::
+
+    <script
+        tal:attributes="nonce request/client/client_nonce"
+        src="javascript.js"></script>
+
+for each script, object or style tag.
+
+Remediating ``unsafe-inline``
+-----------------------------
+.. _remediating unsafe-inline:
+
+Using a trusted script to set event handlers to replace the ``onX``
+handlers allows removal of the ``unsafe-inline`` handlers.  If you
+remove ``unsafe-inline`` the ``onX`` handlers will not run. However
+you can use the label provided by the ``onX`` attribute to securely
+enable a callback function.
+
+This method is a work in progress. As an example proof of concept,
+adding this "decorator" script at the end of page.html::
+
+   <script tal:attributes="nonce request/client/client_nonce">
+    /* set submit event listener on forms that have an
+    onsubmit (case insensitive) attribute */
+    forms = document.querySelectorAll(form[onsubmit])
+    for (let form of f) {
+        form.addEventListener('submit',
+                              () => submit_once());
+     };
+   </script>
+
+will set callback for the submit even on any form that has an onsubmit
+attribute to ``submit_once()``. ``submit_once`` is defined in Roundup's
+base_javascript and is generated with a proper nonce.
+
+By including the nonce in the dynamic CSP, we can use our trusted
+"decorator" script to add event listeners. These listeners will call
+the trusted function in base_javascript to replace the ignored ``onX``
+handlers.
+
+.. _CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+
 Configuring native-fts Full Text Search
 =======================================
 
@@ -527,7 +694,7 @@
 10. The ``redis_url`` setting can load a file to better
 secure the url. If you are using redis 6.0 or newer, you can
 specify a username/password and access control lists to
-improv the security of your data. Another good alternative
+improve the security of your data. Another good alternative
 is to talk to redis using a Unix domain socket.
 
 If you are connecting to redis across the network rather
@@ -586,7 +753,7 @@
 Configuring Authentication Header/Variable
 ------------------------------------------
 
-The front end server running roundup can perform the user
+The front end server running Roundup can perform the user
 authentication. It pass the authenticated username to the backend in a
 variable. By default roundup looks for the ``REMOTE_USER`` variable
 This can be changed by setting the parameter ``http_auth_header`` in the

Roundup Issue Tracker: http://roundup-tracker.org/