This is still a work in progress. The standard roundup templates use inline onclick handlers which aren't supported in newer browsers (e.g. chrome) when using nonces to identify valid code.
Basic idea:
Roundup 1.6.0 and newer provide a client_nonce property for the client object. This property is unique for that http connection. Unlike the anti-csrf nonces the client_nonce has no lifetime and is not recorded on the roundup server.
The client_nonce is included in the Content-Security-Policy (CSP) header for the http connection.
All script tags include the nonce= property set to the client_nonce.
- These tell the browser that it is safe to run the code in the script tag as it has been supplied ("signed") by a trusted nonce as supplied in the CSP header.
Code injected from other sources don't have access to the client_nonce and won't be run.
Add CSP header
To generate the CSP headers, put the following into a file in the extensions directory of your tracker:
1 default_security_headers= {
2 'Content-Security-Policy': "object-src 'none';\
3 base-uri 'self'; script-src 'nonce-{nonce}' 'unsafe-inline' \
4 'strict-dynamic' https:; frame-ancestors 'self'; \
5 report-uri https://CSP.example.com/cgi/submit_csp.cgi",
6 'Referrer-Policy': 'same-origin',
7 'X-Content-Type-Options': 'nosniff',
8 'X-Frame-Options': 'SAMEORIGIN',
9 'X-Random': '{rnddata}',
10 'X-XSS-Protection': '1; mode=block',
11 'X-Content-Type-Options': 'nosniff'
12 }
13
14 def AddHtmlHeaders(client, header_dict={}):
15 ''' Generate https headers from dict use default security headers
16
17 Setting the header with a value of None will not inject the
18 header and can override the default set.
19
20 Header values will be formatted with a dictionary including a
21 nonce. Use to set a nonce for inline scripts.
22 '''
23 import random
24
25 try:
26 if client.client_nonce is None:
27 client.client_nonce = client.session_api._gen_sid()
28 except AttributeError:
29 # client.client_nonce doesn't exist, create it
30 client.client_nonce = client.session_api._gen_sid()
31
32 # use for random length padding in headers see BREACH and other type attacks.
33 random.seed()
34 len = int( 1 + 511 * random.random())
35 rnddata= ''.join([ 'X' for n in range( len ) ])
36
37 headers = default_security_headers.copy()
38 headers.update(header_dict)
39
40 client_headers = client.additional_headers
41
42 for header, value in list(headers.items()):
43 if value is None:
44 continue
45 client_headers[header] = value.format(nonce=client.client_nonce, rnddata=rnddata)
46
47 def init(instance):
48 instance.registerUtil('AddHtmlHeaders', AddHtmlHeaders)
Then call utils.AddHtmlHeaders from page.html like so:
<html lang="en"> <tal:code tal:content="python:utils.AddHtmlHeaders(request.client,{'Content-Security-Policy': None})" /> <head>
from inside the icing macro definition (note line 1 and 3 should already exist).
You can put it elsewhere in the header, but I tried to run it as the first element of the page. Since the icing macro in page.html is called to display most content, this handles all items except the popup helpers. You may need to add a call to AddHtmlHeaders to any page that has the string '<html' in it. These pages do not use the icing macro to provide the html and head tag. In the classic tracker template this includes the pages:
_generic.calendar.html:<html> _generic.help-empty.html:<html> _generic.help.html:<html tal:define="property request/form/property/value" > _generic.help-list.html:<html tal:define="vok context/is_view_ok"> _generic.help-search.html:<html> _generic.help-submit.html:<html> _generic.keywords_expr.html:<html> page.html:<html> user.help.html:<html tal:define="property request/form/property/value; user.help-search.html:<html
Overwrite the existing helper links
As mentioned above, onclick handlers are generated by the roundup core to run javascript helpers for selecting users in nosy lists or provide a calendar popup (e.g. the (list) or (cal) links). When a nonce has been included in the CSP, the unsafe-inline directive no longer works in some browsers. There is no (useful 1) way to sign a handler like onclick. Instead the click handler has to be replaced by a nonce- signed javascript script.
To make this possible the handler needs three things:
- the URL to load in the new popup window
- the width of the new popup window
- the height of the new popup window
In roundup-2.0 these parameters are made available on the classhelp or calendar links generated by:
tal:replace="structure python:db.issue.classhelp('id,title', property='superseder')"
or
tal:attributes="href python:'''javascript:help_window('issue? @template=calendar&property=%s&form=itemSynopsis', 300, 200)'''%name">(cal)</a>
using data attributes of:
Attribute
Description
data-helpurl
url for classhelp popup
data-calurl
url used for calendar popup. Different from helpurl to allow javascript to identify calendar vs. classhelp links.
data-width
width of the popup window
data-height
height of the popup window
A javascript function to remove the roundup supplied onclick handler and href links and put in new ones:
function ApplyClassHelp() { /* Remove the roundup generated onclick handler */ document.querySelectorAll(".classhelp").forEach(link => link.onclick = null); /* Change href to fragment that doesn't exist. Prevents page reload on click. */ document.querySelectorAll(".classhelp").forEach(link => link.href = "#0help"); /* handle class helpers */ document.querySelectorAll(".classhelp[data-helpurl]").forEach(link => link.onclick = function(event) { help_window(this.dataset.helpurl, this.dataset.width, this.dataset.height); event.preventDefault(); /* Link changing to non-existent fragment should prevent navigation/reload. As should preventDefault call. But return false just to be safe */ return false; }); document.querySelectorAll(".classhelp[data-calurl]").forEach(link => link.onclick = function(event) { help_window(this.dataset.calurl, this.dataset.width, this.dataset.height); event.preventDefault(); /* Link changing to non-existent fragment should prevent navigation/reload. As should preventDefault call. But return false just to be safe */ return false; }); }
Same but written using the jQuery library:
function ApplyClassHelp() { /* With nonce in the csp inline onclick handlers don't work anymore. Unsafe-inline is ignored if nonce- or sha256- are included. So add the onclick handlers to classhelp dynamically. */ jQuery('.classhelp').prop('onclick', null); /* Change href to fragment that doesn't exist. Prevents page reload on click. */ jQuery('.classhelp').prop('href', "#0help"); /* handle class helpers */ jQuery('.classhelp[data-helpurl]').click(function(event) { help_window(this.dataset.helpurl, this.dataset.width, this.dataset.height); event.preventDefault(); /* Link changing to non-existent fragment should prevent navigation/reload. As should preventDefault call. But return false just to be safe */ return false; }); /* handle calendar helpers */ jQuery('.classhelp[data-calurl]').click(function(event) { help_window(this.dataset.calurl, this.dataset.width, this.dataset.height); event.preventDefault(); /* Link changing to non-existent fragment should prevent navigation/reload. As should preventDefault call. But return false just to be safe */ return false; }); }
Tying it all together
The last step is to run the ApplyClassHelp function when the page has finished loading (is ready). To do this add javascript at the end of page.html. For this example, put the javascript function into the file: html/js/BaseJavascript.js. Append the following bit of javascript (untested):
document.addEventListener('readystatechange', (event) => { ApplyClassHelp(); }); document.addEventListener('DOMContentLoaded', (event) => { ApplyClassHelp(); });
or its jQuery equivalent if you are using jQuery:
jQuery(document).ready(function () { ApplyClassHelp(); });
Then at the end of page.html before the closing <body> tag add:
<script tal:attributes="nonce request/client/client_nonce" src="@@file/js/BaseJavascript.js"></script>
Note that this may need to be added to other html templates that have the <html tag and use AddHtmlHeaders as discussed above.
We could compute the sha256 checksum of all the onclick handlers and include that somehow in the CSP header (1)