Roundup Tracker

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:

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

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:

  1. the URL to load in the new popup window
  2. the width of the new popup window
  3. 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:

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.

  1. We could compute the sha256 checksum of all the onclick handlers and include that somehow in the CSP header (1)