JavaScript security best practices

Overview #

The primary vulnerability we need to be careful of in JavaScript is Cross Site Scripting (XSS). In WordPress with PHP, we use escaping functions to avoid that — esc_html(), esc_attr(), esc_url(), etc. Given that, it only seems natural that we would also need to escape HTML in JavaScript.

As it turns out out, however, that’s the wrong way to approach JavaScript security. To avoid XSS, we want to avoid inserting HTML directly into the document and instead, programmatically create DOM nodes and append them to the DOM. This means avoiding .html(), .innerHTML, and other related functions, and instead using .append(), .prepend(), .before(), .after(), and so on.

Here is an example:

    url: ''
}).done( function( data ) {
    var link = '<a href="' + data.url + '">' + data.title + '</a>';

    jQuery( '#my-div' ).html( link );

This approach is dangerous, because we’re trusting that the response from includes only safe data – something we can not guarantee, even if the site is our own. Who is to say that data.title doesn’t contain alert( "haxxored");;?

Instead, the correct approach is to create a new DOM node programmatically, then attach it to the DOM:

    url: ''
}).done( function( data ) {
    var a = jQuery( '<a />' );
    a.attr( 'href', data.url );
    a.text( data.title );

    jQuery( '#my-div' ).append( a );

Note: It’s technically faster to insert HTML, because the browser is optimized to parse HTML. The best solution is to minimize insertions of DOM nodes by building larger objects in memory, then insert it into the DOM all at once, when possible.

By passing the data through either jQuery or the browser’s DOM API’s, we ensure the values are properly sanitized and remove the need to inject insecure HTML snippets into the page.

To ensure the security of your application, use the DOM APIs provided by the browser (or jQuery) for all DOM manipulation.

↑ Top ↑

Escaping Dynamic JavaScript Values #

When it comes to sending dynamic data from PHP to JavaScript, care must be taken to ensure values are properly escaped. The core function esc_js() helps escape JavaScript for us in DOM attributes, while all other values should be encoded with json_encode().

From the WP Codex on esc_js():

It is intended to be used for inline JS (in a tag attribute, for example onclick=”…”).

If you’re not working with inline JS in HTML event handler attributes, a more suitable function to use is json_encode, which is built-in to PHP.

This approach is incorrect:

var title = '<?php echo esc_js( $title ); ?>';
var url = '<?php echo esc_js( $url ); ?>';

Depending on the context, it’s better to use rawurlencode() (note that it adds the quotes automatically) or wp_json_encode() in combination with esc_url():

var title = decodeURIComponent( '<?php echo rawurlencode( (string) $title ); ?>' )
var url   = <?php echo wp_json_encode( esc_url( $url ) ) ?>;

For more examples, please see our code sample repository.

↑ Top ↑

Stripping Tags #

It may be tempting to use .html() followed by .text() to strip tags – but this approach is still vulnerable to attack:

// Incorrect
var text = jQuery('
<div />').html( some_html_string ).text();
jQuery( '.some-div' ).html( text );

Setting the HTML of an element will always trigger things like src attributes to be executed, which can lead to attacks like this:

// XSS attack waiting to happen
var some_html_string = '<img src="a" onerror="alert('haxxored');" />';

As soon as that string is set as a DOM element’s HTML (even if it’s not currently attached to the DOM!), src will be loaded, will error out, and the code in the onerror handler will be executed…all before .text() is ever called.

The need to strip tags is often indicative of bad practices – remember, always use the appropriate API for DOM manipulation.

// Correct
jQuery( '.some-div' ).text( some_html_string );

Here are some things to watch out for:

// jQuery passes the values in these methods through to eval():
$('#my-div').after('<script>alert("1. XSS Attack with jQuery after()");</script>');
$('#my-div').append('<script>alert("2. XSS Attack with jQuery append()");</script>');
$('#my-div').before('<script>alert("3. XSS Attack with jQuery before()");</script>');
$('#my-div').html('<script>alert("4. XSS Attack with jQuery html()");</script>');
$('#my-div').prepend('<script>alert("5. XSS Attack with jQuery prepend()");</script>');
$('#my-div').replaceWith('<div id="my-div"><script>alert("6. XSS Attack with jQuery replaceWith()");</script></div>');

// And these:
$('<script>alert("7. XSS Attack with jQuery appendTo()");</script>').appendTo('#my-div');
$('<script>alert("8. XSS Attack with jQuery insertAfter()");</script>').insertAfter('#my-div');
$('<script>alert("9. XSS Attack with jQuery insertBefore()");</script>').insertBefore('#my-div');
$('<script>alert("10. XSS Attack with jQuery prependTo()");</script>').prependTo('#my-div');
$('<div id="my-div"><script>alert("11. XSS Attack with jQuery replaceAll()");</script></div>').replaceAll('#my-div');

// Plain JS will also evaluate these:
document.write('<script>alert("12. XSS Attack with document.write()");</script>');
document.writeln('<script>alert("13. XSS Attack with document.writeln()");</script>');

// Script elements inserted using innerHTML do not execute when they are inserted:
var div = document.createElement('div');

div.innerHTML = 'Foo<script>alert("No attack with script innerHTML");</script>';
div.innerHTML = 'Foo<span></span><script defer>alert("No attack with deferred script innerHTML");</script>';
div.innerHTML = '<scr' + 'ipt>alert("No attack with concat script tag innerHTML");</script>';

// JS prepend() and JS append() are safe when using strings:
var div2 = document.createElement('div');

div2.prepend('<script>alert("No attack with JS prepend()");</script>');
div2.append('<script>alert("No attack with JS append()");</script>');

// ...But not safe when inserting a Node:
var div3 = document.createElement('div');

var newScript = document.createElement("script");
var inlineScript = document.createTextNode("alert('14. XSS Attack with JS append()');");

// Other ways to use innerHTML can cause XSS though:
var div4 = document.createElement('div');

var myImageSrc = 'x" onerror="alert(\'15. XSS Attack with img innerHTML\')';
div.innerHTML = '<img src="' + myImageSrc + '">';

You can run this JSBin to see it in action.

↑ Top ↑

Using encodeURIComponent() #

When using values as part of a URL, for example when adding parameters to a URL or building a mailto: link the JavaScript variables need to be encoded to be correctly interpreted by the browser. Using encodeURIComponent() will ensure that the characters you use will be properly interpreted by the browser. This also helps prevent some trickery such as adding & which may cause the browser to incorrectly interpret the values you were expecting. You can find more information on this on the OWASP website.

↑ Top ↑

Using DOMPurify #

As mentioned above, using jQuery’s .html() function or React’s dangerouslySetInnerHTML() function can open your site to XSS vulnerabilities by treating arbitrary strings as HTML. These functions should be avoided whenever possible. While it’s easy to think content from your own site is “safe,” it can become an attack vector if a user’s account is compromised or if another part of the application is not doing enough validation.

While we recommend that first you try to use structured data and build the HTML inside the JavaScript, that’s not always feasible. If you do need to include HTML strings inside your JavaScript, we recommend using the DOMPurify package to sanitize strings to remove executable elements that could contain attack vectors. This is very similar to how WP_KSES works.

To use DOMPurify you need to include it as such:

 * For Browsers
import DOMPurify from 'dompurify';
// or
const DOMPurify = require('dompurify');

 * For Node.js we need JSDOM's window
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom'); 
const window = (new JSDOM('')).window;
const DOMPurify = createDOMPurify(window);

You can then call it as such:

const clean = DOMPurify.sanitize(dirty);

Here are a few examples taken from the DOMPurify Readme:

DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x">
abc<iframe/\/src=jAva&amp;Tab;script:alert(3)>def'); // becomes 



<math><mi//xlink:href="data:x,<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3Ealert(4)%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&amp;lt;script&amp;gt;" title="&amp;lt;script&amp;gt;" />">'); // becomes 






</TABL>'); // becomes 

// =>








<li><A HREF=//>click</UL>

'); // becomes 


<li><a href="//">click</a></li>



↑ Top ↑

Other Common XSS Vectors #

  • Using eval(). Never do this.
  • Un-safelisted / un-sanitized data from URLs, URL fragments, query strings, cookies.
  • Including untrusted / unreviewed third-party JavaScript libraries.
  • Using outdated / unpatched third-party JavaScript libraries.

Helpful Resources

Ready to get started?

Drop us a note.

No matter where you are in the planning process, we’re happy to help, and we’re actual humans here on the other side of the form. 👋 We’re here to discuss your challenges and plans, evaluate your existing resources or a potential partner, or even make some initial recommendations. And, of course, we’re here to help any time you’re in the market for some robust WordPress awesomeness.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.