How to reduce the threat from third-party includes

To achieve better-looking, more feature-rich and responsive applications, there is an ever-growing need to include resources from 3rd party domains into your web application. Common examples are JavaScript frameworks like jQuery or AngularJS, often distributed via a content delivery network (CDN), or even complete applications like Google Maps. But by including these resources, the security boundary of the web application is extended to include these 3rd party domains. If they get compromised, the delivered resources my be modified or backdoored, thus injecting malicious code into your website.

Fortunately, two browser-side mechanisms have been introduced to mitigate or limit the impact from externally-included resources: Subresource Integrity and sandboxing.

Subresource Integrity

Subresource Integrity (SRI) allows developers to pin down certain versions of scripts or stylesheets which are included from external domains. The goal is to ensure that the external script has not been modified inadvertently, or intentionally replaced or altered by an attacker. A cryptographic hash is used to ensure that the version included in the website has not been tampered with:

<script src="https://external.cdn.ch/example-framework.js" 
integrity="sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYX
ysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" 
crossorigin="anonymous"></script>

As soon as the content of the external resource “example-framework.js” changes, the hash set on the consuming website does no longer match the script’s hash and the script will not be run by the browser. Note that Cross-Origin Resource Sharing (CORS) must be enabled on the Content Delivery Network’s side to be able to use the integrity attribute, or else the script will not load at all, even with a correct hash.

The SHA-512 hash to pin down external resources can be calculated as follows:

cat example-framework.js | openssl dgst -sha512 -binary | 
openssl base64 -A

While only the script and link tags support the integrity attribute by the time of writing, other tags will probably follow, enabling developers to also ensure the integrity of images or other content embedded from externally.

The crossorigin attribute defines whether or not the browser should send credentials when fetching the external resource. It defaults to anonymous if an invalid value is given. The values are defined as follows:

Keyword State Description
anonymous (or empty string) Anonymous Requests for the element will have their mode set to “cors” and their credentials mode set to “same-origin”. (i.e. CORS request and credentials for the external domain are not sent with the request)
use-credentials Use Credentials Requests for the element will have their mode set to “cors” and their credentials mode set to “include”. (i.e. CORS request and credentials for the external domain are sent with the request)
[crossorigin attribute not present] No CORS Requests for the element will have their mode set to “no-cors”. As a consequence, data cannot be read cross-origin.

Security considerations

SRI only makes sense if the website is secured with TLS. If an attacker is able to intercept and alter the website he can inject his own scripts and strip the hashes or generate them on-the-fly.

Since hashes like MD5 or SHA-1 are prone to collision attacks, they should be avoided for pinning external resources. SHA-384 or SHA-512 are considered secure.

Note that only Firefox, Chrome and Opera support SRI at the time of writing. This means that users of Internet Explorer or Edge do not benefit from the SRI protection. Therefore it is still preferable to host the resources on your own domain, than to depend on SRI.

While SRI can be used to ensure the integrity of external resources, it does not feature a report mechanism that tells the including party if a script has been modified and therefore does no longer run in the user’s browsers. A report-uri mechanism, as known from HSTS, HPKP or CSP, would need to be custom-developed.

Sandboxing

From a security standpoint, it is a bad idea to include external resources directly into the website, because this is giving them full access to the website’s Document Object Model (DOM). A resource could thus access and alter the whole content of the website. When confined in an iframe, access to the parent’s DOM is restricted. It is however still possible to open pop-up windows from within the iframe, display dialogs or perform other unwanted actions. With the HTML5 feature sandboxing, it is possible to further restrict the behavior of “iframed” content.

The following examples would embed content into an iframe, while disabling JavaScript, applying a unique origin to its content and preventing it from using the top-level browsing context:

<iframe src="//external.cdn.ch/example_resource.html" 
id="sandboxed_frame" sandbox="" height="500" width="700"></iframe>
<iframe src="//external.cdn.ch/example_resource.html" 
id="sandboxed_frame" sandbox height="500" width="700"></iframe>

An empty or missing “sandbox” value applies the sandbox as restrictively as possible. This would prevent any execution of JavaScript in the iframe. It is however possible to soften up the sandbox with various flags as shown in the following table, to allow dynamic and interactive content:

Directive Effect
allow-popups Enables iframe content to open pop-up windows.
allow-pointer-lock Enables iframe content to use the Pointer Lock API. The mouse movements can be tracked and the mouse pointer can be hidden.
allow-scripts Enables iframe content to run JavaScript (with the exception of code which must be enabled through further flags, e.g. window.open())
allow-popups-to-escape-sandbox Enables iframe content to create pop-up windows without sandbox restrictions. (Why would you want to do that?!?)
allow-modals Enables iframe content to display modal windows such as alert();, prompt(); or through showModal().
allow-top-navigation Enables iframe content to load content to the top-level browsing context, e.g. via href or target=”_top”.
allow-same-origin

Sandboxed iframe content resides in a custom origin, isolating it from the originating domain. It can therefore not perform requests or read data from its original origin. This directive allows the iframe content to run in the originating domain, thus removing those restrictions.

allow-forms Enables iframe content to submit forms.
allow-presentation Enables iframe content to start presentations.

As always, the sandboxing directive should be set as restrictively as possible. All major browsers support the sandbox feature. While the execution of JavaScript cannot be avoided for non-static content, sandboxing can nonetheless prove valuable to prevent those scripts from opening pop-up windows, dialogues or screen-filling content which could be abused to trick users into revealing their credentials.

Our next Web Application Security courses

Risks of DOM Based XSS due to “unsafe” JavaScript functions

Introduction

Several native JavaScript functions or properties like .eval() and .innerHTML as well as several jQuery functions like .html() and .append() are considered as “unsafe”, but why? The reason is that they allow DOM manipulation using strings containing HTML code (e.g.”<b>This text is bold</b>“), which can lead to DOM Based Cross-Site Scripting vulnerabilities. To be more specific: The usage of such functions is not a problem as long as no user input is directly embedded in an “unsafe” way. jQuery can help us to safely manipulate the DOM without executing XSS in user defined inputs. But do not by mistake assume jQuery is safe per se, it only provides us some helper function to manipulate the DOM more safely.

The subsequent sections show the difference between safe and unsafe usage of JavaScript and jQuery functions in the following scenarios:

Unsafe DOM manipulation using eval():

var txtField = "field1";
var txtUserInput = "'test@csnc.ch';alert(1);";
eval(
   "document.forms[0]." + txtField + ".value =" + txtUserInput
);

The last double quote causes the user input to be treated as JavaScript. This results in the following JavaScript code being executed by eval():

document.forms[0].field1.value = 'test@csnc.ch';alert(1);

Therefore the user input is executed:


Safe DOM manipulation using eval():

var txtField = "field1";
var txtUserInput = "'test@csnc.ch';alert(1);";
eval(
   "document.forms[0]." + txtField + ".value = txtUserInput"
);

The double quote at the end causes the user input NOT to be treated as JavaScript. This results in the following JavaScript code being executed by eval():

document.forms[0].field1.value = txtUserInput

Or in other words:

document.forms[0].field1.value = "'test@csnc.ch';alert(1);"

This results in the following HTML code:

<input type='text' id='field1' name='field1'
       value="'test@csnc.ch';alert(1);" />

Therefore the user input is not executed:

However, this snippet shows again how small the difference is between safe and unsafe usage of eval():

"document.forms[0]." + txtField + ".value =" + txtUserInput
"document.forms[0]." + txtField + ".value = txtUserInput"

Therefore it is recommended to completely ban this function from your JavaScript code.

Unsafe DOM manipulation using jQuery html():

var txtAlertMsg = "This is bold: ";
var txtUserInput = "test<script>alert(1)<\/script>";
$("#message").html(
   txtAlertMsg +"<b>" + txtUserInput + "</b>"
);

Or in other words:

$("#message").html(
   "This is bold: <b>test<script>alert(1)<\/script></b>"
);

This results in the following HTML code:

<div id='message'><b>test<script>alert(1)</script></b></div>

Therefore the user input is executed:


Safe DOM manipulation using jQuery html() and text():

var txtAlertMsg = "This is bold: ";
var txtUserInput = "test<script>alert(1)<\/script>";
$("#message").html(
   txtAlertMsg +"<b><div id='userInput'></div></b>"
);
$("#userInput").text(
   txtUserInput
);

Or in other words:

$("#userInput").text(
   "test<script>alert(1)<\/script>"
);

This results in the following HTML code:

<div id='message'>This is bold: <b>
   <div id='userInput'>test&lt;script&gt;alert(1)&lt;/script&gt;</div>
</b></div>

Therefore the user input is not executed:

Conclusion

  • eval() is evil
  • jQuery does not solve all your problems
  • When using JavaScript or jQuery functions to manipulate your DOM you always need to know if your content may contain user input. If yes you must only use functions which encode HTML / JavaScript strings like jQuery text().

Resources