@@ -286,6 +286,173 @@ get the gzip version and not a brotli compressed version. This
286286mechanism allows the admin to allow use of brotli and zstd for
287287dynamic content, but not for static content.
288288
289+ Adding a Web Content Security Policy (CSP)
290+ ==========================================
291+
292+ A Content Security Policy (`CSP`_) adds a layer of security to
293+ Roundup's web interface. It makes it more difficult for an
294+ attacker to compromise Roundup. By default Roundup does not add
295+ a CSP. If you need to implement a CSP, this section will help you
296+ understand how to add one and document the current level of
297+ support for CSP in Roundup.
298+
299+ Roundup's web interface has remained mostly unchanged since it
300+ was created over a decade ago. Current releases have been slowly
301+ modernizing the HTML to improve security. There are still some
302+ improvements that need to happen before the tightest CSP
303+ configurations can be used.
304+
305+ Writing a CSP is complex. This section just touches on how to
306+ create and install a CSP to improve security. Some of it might
307+ break functionality.
308+
309+ There are two ways to add a CSP:
310+
311+ 1. a fixed CSP added by a server
312+ 2. a dynamic CSP added by Roundup
313+
314+ Fixed CSP
315+ ---------
316+
317+ If you are using a web server (Apache, Nginx) to run Roundup, you can
318+ add a ``Content-Security-Policy`` header using that server. WSGI
319+ servers like uWSGI can also be configured to add headers. An example
320+ header would look like::
321+
322+ Content-Security-Policy: default-src 'self' 'unsafe-inline' 'strict-dynamic';
323+
324+ One thing that may need to be included is the ``unsafe-inline``.
325+ The default templates use ``onload``, ``onchange``, ``onsubmit``,
326+ and ``onclick`` JavaScript handlers. Without ``unsafe-inline``
327+ these won't work and popup helpers will not work. Sadly the use
328+ of ``unsafe-inline`` is a pretty big hole in this CSP. You can
329+ set the hashes for all the JavaScript handlers in the CSP. Then
330+ replace ``unsafe-inline`` with ``unsafe-hashes`` to help close
331+ this hole, but has its own issues. See `remediating
332+ unsafe-inline`_ for another way to mitigate this.
333+
334+ The inclusion of ``strict-dynamic`` allows trusted JavaScript
335+ files that are downloaded from Roundup to make changes to the web
336+ interface. These changes are also trusted code that will be run
337+ when invoked.
338+
339+ More secure CSPs can also be created. However because of the ability
340+ to customize the web interface, it is difficult to provide guidance.
341+
342+ Dynamic CSP
343+ -----------
344+
345+ Roundup creates a cryptographic nonce for every client request. The
346+ nonce is the value of the ``client.client_nonce`` property.
347+
348+ By changing the templates to use the nonce, we can better secure the
349+ Roundup instance. However the nonce has to be set in the CSP returned
350+ by Roundup.
351+
352+ One way to do this is to add a templating utility to the extensions
353+ directory that generates the CSP on the fly. For example::
354+
355+ default_security_headers = {
356+ 'Content-Security-Policy': (
357+ "default-src 'self'; "
358+ "base-uri 'self'; "
359+ "script-src https: 'nonce-{nonce}' 'strict-dynamic'; "
360+ "style-src 'self' 'nonce-{nonce}'; "
361+ "img-src 'self' data:; "
362+ "frame-ancestors 'self'; "
363+ "object-src 'self' 'nonce-{nonce}'; "
364+ ),
365+ }
366+
367+
368+ def AddHtmlHeaders(client, header_dict=None):
369+ ''' Generate https headers from dict use default security headers
370+
371+ Setting the header with a value of None will not inject the
372+ header and can override the default set.
373+
374+ Header values will be formatted with a dictionary including a
375+ nonce. Use to set a nonce for inline scripts.
376+ '''
377+ try:
378+ if client.client_nonce is None:
379+ # logger.warning("client_nonce is None")
380+ client.client_nonce = client.session_api._gen_sid()
381+ except AttributeError:
382+ # client.client_nonce doesn't exist, create it
383+ # logger.warning("client_nonce does not exist, creating")
384+ client.client_nonce = client.session_api._gen_sid()
385+
386+ headers = default_security_headers.copy()
387+ if isinstance(header_dict, dict):
388+ headers.update(header_dict)
389+
390+ client_headers = client.additional_headers
391+
392+ for header, value in list(headers.items()):
393+ if value is None:
394+ continue
395+ client_headers[header] = value.format(
396+ nonce=client.client_nonce)
397+
398+ def init(instance):
399+ instance.registerUtil('AddHtmlHeaders', AddHtmlHeaders)
400+
401+
402+ Adding the following to ``page.html`` right after the opening
403+ ``<html....`>`` tag::
404+
405+ <tal:code tal:content="python:utils.AddHtmlHeaders(request.client)" />
406+
407+ will invoke ``AddHtmlHeaders()`` to add the CSP header with the nonce.
408+
409+ With this set of CSP headers, all style, script and object tags will
410+ need a ``nonce`` attribute. This can be added by changing::
411+
412+ <script src="javascript.js"></script>
413+
414+ to::
415+
416+ <script
417+ tal:attributes="nonce request/client/client_nonce"
418+ src="javascript.js"></script>
419+
420+ for each script, object or style tag.
421+
422+ Remediating ``unsafe-inline``
423+ -----------------------------
424+ .. _remediating unsafe-inline:
425+
426+ Using a trusted script to set event handlers to replace the ``onX``
427+ handlers allows removal of the ``unsafe-inline`` handlers. If you
428+ remove ``unsafe-inline`` the ``onX`` handlers will not run. However
429+ you can use the label provided by the ``onX`` attribute to securely
430+ enable a callback function.
431+
432+ This method is a work in progress. As an example proof of concept,
433+ adding this "decorator" script at the end of page.html::
434+
435+ <script tal:attributes="nonce request/client/client_nonce">
436+ /* set submit event listener on forms that have an
437+ onsubmit (case insensitive) attribute */
438+ forms = document.querySelectorAll(form[onsubmit])
439+ for (let form of f) {
440+ form.addEventListener('submit',
441+ () => submit_once());
442+ };
443+ </script>
444+
445+ will set callback for the submit even on any form that has an onsubmit
446+ attribute to ``submit_once()``. ``submit_once`` is defined in Roundup's
447+ base_javascript and is generated with a proper nonce.
448+
449+ By including the nonce in the dynamic CSP, we can use our trusted
450+ "decorator" script to add event listeners. These listeners will call
451+ the trusted function in base_javascript to replace the ignored ``onX``
452+ handlers.
453+
454+ .. _CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
455+
289456Configuring native-fts Full Text Search
290457=======================================
291458
@@ -527,7 +694,7 @@ port 7200 using the password ``mypassword`` to open database
52769410. The ``redis_url`` setting can load a file to better
528695secure the url. If you are using redis 6.0 or newer, you can
529696specify a username/password and access control lists to
530- improv the security of your data. Another good alternative
697+ improve the security of your data. Another good alternative
531698is to talk to redis using a Unix domain socket.
532699
533700If you are connecting to redis across the network rather
@@ -586,7 +753,7 @@ may be found in the `customisation documentation`_.
586753Configuring Authentication Header/Variable
587754------------------------------------------
588755
589- The front end server running roundup can perform the user
756+ The front end server running Roundup can perform the user
590757authentication. It pass the authenticated username to the backend in a
591758variable. By default roundup looks for the ``REMOTE_USER`` variable
592759This can be changed by setting the parameter ``http_auth_header`` in the
0 commit comments