Content Security Policy Development Best Practices

Documentation
A Content Security Policy (CSP) is a standard header (Content-Security-Policy) sent by the server to convey various security-related directives to the browser.

This topic covers background and steps that developers using the LabKey platform may need to take to allow the configuration of a strict Content Security Policy. Anyone who authors or maintains HTML and JavaScript (via Java modules, file-based modules, wikis, static resources, etc.) that's executed on a LabKey product should review these best practices and ensure their code follows it.

Learn more about how administrators configure and customize the CSP within a running server here:

Default Content Security Policy on Development Instances

By default, a LabKey development environment configures strict enforce and report CSPs. You can confirm the active CSP by examining the Headers using the Network tab in your browser's development tools.

The enforce CSP will cause the browser to block all directive violations; violations of the report CSP will cause the browser to simply report them to the browser console and to the server's ContentSecurityPolicyReport action, but these will not be blocked. The standard CSPs include substitution parameters for allowed resource hosts, script nonce, and extra parameters for the report action.

Note: The standard development CSPs are in the process of being tested and rolled out to client deployments. We expect them to evolve over time and become increasingly strict. Before configuring a CSP on a production environment, we recommend in-depth review of the directives and thorough testing on a staging server. Your infrastructure, organizational policies, and content deployed on your server dictate what CSP directives should be configured.

The key directive for XSS purposes is script-src:

  • 'nonce-${REQUEST.SCRIPT.NONCE}': This instructs the browser to block all JavaScript except for code contained in a <script> tag that specifies a correct nonce value. A unique nonce value is generated on every request and included (via substitution) in the header.
  • Not including 'unsafe-inline' means inline script is not allowed. In other words, all inline event handlers are blocked. This is key to blocking all XSS attempts. Event handlers must be attached to elements via JavaScript code within a nonce-tagged script block, which is impossible for attackers to do, since they don't know the request-specific nonce value.
We strongly recommend that developers run their local instances with a csp.enforce policy. This blocks inline scripts and other forbidden actions, just as they would be blocked on your production server. In LabKey's development and testing, we have found Firefox to be the most strict browser and generally prefer it when doing CSP-related work. As an example, Firefox warns of static inline handlers at page load time whereas Chrome warns only when invocation is actually attempted.

Note that you can view all CSP violations reported to your local deployment and (if you have access to TeamCity) within each TeamCity suite; check csp-report.log in your LabKey logs location or in a suite's artifacts on TeamCity. You can also view the log file via > Site > Admin Console > View CSP Report Log File. Each reported violation will appear in the log file; there may be duplicates. As you fix issues, you may want to periodically delete this file to purge obsolete reports.

Specify Nonces on Script Tags

Every HTML script tag requires a nonce with a value that matches the request-specific value in the response CSP header. The server generates this random value, substitutes it into the CSP that gets sent with the response, and substitutes it into all script tags that gets sent in the response. Developers are responsible for adding an appropriate placeholder for the server to use for substitution purposes. The exact syntax varies based on the type of file:

Module / Assay / Participant HTML views

<script type="text/javascript" nonce="<%=scriptNonce%>">

JSPs

<script type="text/javascript" nonce="<%=getScriptNonce()%>">

Another option in JSPs is to use the labkey:script tag, which automatically adds the nonce attribute:

<labkey:script>

HTML wiki pages

Nonces are not required in wiki page script tags; nonce attributes are added automatically. If provided in the source, though, a nonce attribute will be tolerated; it will simply be replaced.
<!-- Nonce is not required; server will inject nonce into all wiki script tags -->
<script type="text/javascript">
...
</script>

<!-- Though nonce is tolerated, if provided; server will replace it -->
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
...
</script>

Java code

PageConfig is responsible for providing the current nonce value (and collecting event handlers, see below); here's Java code that generates a valid script tag:

out.write("<script type=\"text/javascript\" nonce=\"" + HttpView.currentPageConfig().getScriptNonce() + "\">");

Static HTML pages

There's no way to inject a dynamically substituted nonce value into a static HTML page. Pages previously placed in the webapps directory may need action. If the page requires JavaScript (for event handlers or any other reason) then either:
  • An associated standalone js file will be required.
  • The page should instead be a module HTML view. HTML files placed in resources/views will get nonces added (and other values get substituted).

Migrate Inline Event Handlers

A strict (no 'unsafe-inline' directive) CSP forbids all inline events. Instead, events must be attached via JavaScript inside a properly nonced script tag. Approaches vary based on the type of file.

Module / Assay / Participant HTML views and HTML wiki pages

If you have inline event handlers, they are forbidden by strict CSP. Two examples here, a "myInput" that runs a "respondToChange" function onchange, and a "reload" link that takes an action on click.

<!-- These inline event handlers are forbidden by strict CSP -->
<input id="myInput" onchange="respondToChange()">

<a href="#" id="reload" onclick = "window.location.reload()">Reload</a>

The standard way to migrate them looks like the following. You use a unique id (name) for the element (adding one if it doesn't already have one), and move the event handler to a LABKEY.Utils.onReady function, naming the action that will invoke it. The two styles shown here show "myInput" running the "respondToChange" that is defined later in the script and "reload" putting the window.location.reload into an inline function. If an element needs different handlers for different actions, i.e. both an onchange and an onclick, they can be included as separate rows. You can this new handler section to any existing script section right in the wiki or create a new one to include it.

<input id="myInput">
<a href="#" id="reload">Reload</a>

<!-- nonce is required for module HTML views and recommended for wiki pages -->
<script type="text/javascript" nonce="<%=scriptNonce%>">

LABKEY.Utils.onReady(function() {
document.getElementById("myInput")['onchange'] = respondToChange;
document.getElementById("reload")['onclick'] = function(){ window.location.reload(); };
});

<!-- respondToChange() would also be defined in this script section -->

</script>

If the element doesn't have a unique ID you'll have to give it one, or come up with a different approach to retrieve the element.

While there are other options for assigning a handler to an element, we tend to use the "['onclick'] = handler" syntax to help eliminate migrated handlers from regex searches that are performed across the code base.

JSPs

The syntax above works just fine in JSPs and is often the preferred approach. But JSPs offer many other viable options for migrating inline handlers, including:

  • Use element builders instead of hand-coding HTML and attaching handlers manually. These builders know how to register handlers correctly; they also ensure proper encoding and syntax. The migrated code is always more succinct and readable. Strongly consider migrating hand-coded elements to <%=select()%>, <%=link()%>, <%=input()%>, etc.
  • Change a <form> to <labkey:form> or <input> to <labkey:input>; these custom tags attach events correctly.
<!-- LabKey JSP tabs attach events correctly -->
<labkey:input id="myInput" onChange="respondToChange()">
  • Use the addHander() method, a more succinct option vs. document.getElementById()
<!-- Can be placed immediately before or after the associated element -->
<% addHandler("myInput", "change", "respondToChange()"); %>
<input id="myInput">
  • If you need to attach a handler to many elements (e.g., one per row in a grid or generating elements in a loop), PageConfig.addHandlerForQuerySelector() is an option. Find usages on this method for examples. From a JSP:
<!-- Attach an onerror event to every img tag with class "labkey-flow-graph" -->
getPageConfig().addHandlerForQuerySelector("IMG.labkey-flow-graph", "error", "flowImgError(this);");

JavaScript and ExtJS

JavaScript code that generates and assigns dynamic HTML can be tricky to migrate away from inline handlers. The handlers must be attached after the HTML is injected into the DOM, which can be far removed from the code that initially generates the HTML. The best approach varies widely, but here are a couple tips:

  • ExtJS: the "afterrender" event tends to be a good place to attach event handlers since the elements should be available
  • LABKEY.Utils has a couple helper methods: LABKEY.Utils.attachEventHandler() and LABKEY.Utils.attachEventHandlerForQuerySelector()

Java code

Most of the JSP approaches are appropriate for straight Java code. Of course, tags and JspBase helper methods aren't available, but you can use SelectBuilder, LinkBuilder, InputBuilder, etc. directly. The current PageConfig can be obtained via HttpView, so, to attach an event handler:

HttpView.currentPageConfig().addHandler("myButton", "click", "doSomething()");

Restrict/Allow External Connections

To protect users, most standard directives restrict the browser's ability to load resources from external sources. By default, only sources from the LabKey Server are allowed. If your code needs to allow external resources then you can register them via Java code:

ContentSecurityPolicyFilter.registerAllowedSources()

The registered hosts are added to the CSP via the ${CONNECTION.SOURCES}, ${IMAGE.SOURCES}, ${FRAME.SOURCES}, and other substitution parameters.

Note that administrators can register external resource hosts via the web UI, which can be useful for resources required by wiki pages or file-based modules. Configure the list of allowed hosts via: > Site > Admin Console > Allowed External Resource Hosts. Any hosts added to this list will also be substituted into the CSP.

Scan Wikis, Participant Views, and Study Descriptions.

CSP reports will only be raised for pages that are accessed. The Wiki Validator is the first place to start checking that wikis do not contain inline handlers, however, there may be other issues in wikis that would only be found when viewed later. Participant views and study descriptions that may have script tags missing nonces will also only raise CSP reports when viewed.

To assist users with many wikis and/or participant views in checking proactively for violations, a scanner utility can be accessed by editing the URL for the desired container to end in:

cloudServices-validator.view
For example:
https://myserver.com/home/cloudServices-validator.view

To check your site:
  • Open Firefox (for best results use a browser like Firefox that proactively flags inline script)
  • Go to the cloudServices-validator.view for the desired container.
  • Open the developer tools in the browser.
  • Use the radio buttons to select the objects and container scope to analyze. The box for Verbose logging is checked by default.
  • Click Go.
  • Watch the browser console for CSP reports about the wikis, participant views, and study descriptions as they are each rendered in the box.
  • Address these reports by adding nonces where they are missing and other changes suggested.
  • Once you've addressed known reports, rerun the utility to confirm and see if anything additional is reported.

It is also possible to expose this validator in a wiki if desired. Note that you may need multiple copies of this wiki, as you can only check one project at a time.

Related Topics

Was this content helpful?

Log in or register an account to provide feedback


previousnext
 
expand allcollapse all