• No results found

Sanitize Input for DOM XSS

In document Secure Your Node.js Web Application (Page 163-168)

DOM-based XSS is a different beast altogether, and it deserves its own section and rules. To get a thorough overview of DOM XSS and sanitizing rules, consult the OWASP DOM-based XSS Prevention Cheat Sheet.12 Also, if you skipped the previous section on various sanitizing rules, then go back. You need to know how to deal with first-order XSS attacks to understand how to deal with DOM XSS.

If you’re using a lot of DOM manipulation in your application, it’s prone to DOM XSS. I recommend using a JavaScript validation library designed for context-specific validations, such as the ESAPI JavaScript library13 from OWASP.

Treat DOM-based XSS sanitizing as a two-step challenge. First, you get the data into a JavaScript variable, as I discussed previously in Rule 3: Escape untrusted data inserted into JavaScript data values., on page 149. Then, you sanitize the data according to the usage.

If you used only the previous rules, you’d wind up with something like the following:

chp-11-xss/xss-dom-simple.ejs

<!DOCTYPE html>

<html>

<head lang="en">

<meta charset="UTF-8">

<title>My DOM XSS</title>

</head>

<body>

<div id="dynamic"></div>

<script>

12. https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet 13. https://code.google.com/p/owasp-esapi-js/

Chapter 11. Fight Cross-Site Scripts

154

// Step 1 - Get data

var text = '<%- ESAPI.encoder().encodeForJS(unsafe) %>';

// Step 2 - Use data

var $element = document.getElementById('dynamic');

$element.innerHTML = text; // <- VULNERABLE! We need HTML encoding

</script>

</body>

</html>

You can fix this by applying reverse encoding on the data-insertion part:

chp-11-xss/xss-dom-simple-fix1.ejs

<% var E = ESAPI.encoder(); %>

// Step 1 - Get data

// Apply reverse order encoding

var text = '<%- E.encodeForJS(E.encodeForHTML(unsafe)) %>';

// Step 2 - Use data

You can also apply encoding at runtime:

chp-11-xss/xss-dom-simple-fix2.ejs

// Step 1 - Get data

var text = '<%- ESAPI.encoder().encodeForJS(unsafe) %>';

// Step 2 - Use data

var $element = document.getElementById('dynamic');

// Encode using client side script

$element.innerHTML = $ESAPI.encoder().encodeForHTML(text);

</script>

</body>

</html>

Unfortunately, you can’t just use the corresponding level of encoding from the previous section when dealing with DOM XSS. The rules are slightly dif-ferent, so let’s take a look.

Rule 0: Use DOM construction methods instead of HTML interpretation.

Untrusted data should be treated only as displayable text. You should never treat untrusted data as code or markup within JavaScript code. In order to construct dynamic HTML interfaces, you should use JavaScript methods designed to construct DOM, such as document.createElement("…"), element.setAt-tribute("…","value"), and element.appendChild(…). Don’t build HTML strings and let the browser interpret them for you.

You don’t have to have a complex environment that’s hard to escape properly, so instead of this

// Get value

var text = '<%- ESAPI.encoder().encodeForJS(unsafe) %>';

// Construct HTML

var input = '<input name="company_name" value="' + text + '" />';

// Insert HTML

var form1 = document.forms[0];

form1.insertAdjacentHTML('beforeend', input);

// or with jquery

$('form:first').append(input);

use DOM methods to remove an interpretation layer and simplify the process:

// Get value

var text = '<%- ESAPI.encoder().encodeForJS(unsafe) %>';

// Construct HTML

var input = document.createElement('input');

input.setAttribute('name', 'company_name');

input.setAttribute('value', text);

// Insert HTML

var form1 = document.forms[0];

Chapter 11. Fight Cross-Site Scripts

156

form1.insertAdjacentHTML('beforeend', input);

// or with jquery

$('form:first').append(input);

The element.setAttribute() is safe for only a limited number of attributes. Dangerous attributes include any attributes that are for a command execution context, such as onclick() or onblur().

Rule 1: JavaScript and HTML encode before HTML subcontext.

Several methods in JavaScript can directly render HTML. When providing untrusted input to these methods, you first have to be sure that it doesn’t break out of the JavaScript context and the HTML context. You can do this by applying the encoding backwards, so you first deal with HTML encoding and then JavaScript encoding. The following list shows some examples of methods used for HTML rendering:

element.innerHTML = "<HTML> Tags and markup";

element.outerHTML = "<HTML> Tags and markup";

document.write("<HTML> Tags and markup");

document.writeln("<HTML> Tags and markup");

Here’s an example of how you can encode using ESAPI and EJS for server-side template rendering:

<%

var htmlEncoded = ESAPI.encoder().encodeForHTML(unsafe);

var jsEncoded = ESAPI.encoder().encodeForJS(htmlEncoded);

%>

element.innerHTML = "<%- jsEncoded %>";

If you get the data directly from the server, such as when you’re using AJAX to get the data for the client side, you can use the following example:

element.innerHTML = $ESAPI.encoder().encodeForHTML(unsafe);

Rule 2: Do not apply attribute encoding in DOM context.

When you’re inserting untrusted input into an attribute value, then you don’t have to attribute escape it from within the DOM context. You just have to worry about JavaScript escape. Using both will break how the value is visu-ally represented. Let’s go over some bad examples, so you know what you shouldn’t do:

var x = document.createElement("input");

x.setAttribute("name", "company_name");

// In the following line of code, companyName represents untrusted user input // The Encoder.encodeForHTMLAttr() is unnecessary and causes double-encoding

Sanitize Input for DOM XSS

157

<%

var attEncoded = ESAPI.encoder().encodeForHTMLAttr(companyName);

var jsEncoded = ESAPI.encoder().encodeForJS(attEncoded);

%>

x.setAttribute("value", '<%- jsEncoded %>');

var form1 = document.forms[0];

form1.appendChild(x);

If companyName had the value Johnson & Johnson, you would see Johnson \&amp; Johnson in the input text field. In this case, you should use only JavaScript encoding to prevent an attacker from closing out the single quotes and inserting code or escaping to HTML and opening a new <script> tag. Let’s do this correctly by encoding only for JavaScript:

var x = document.createElement("input");

x.setAttribute("name", "company_name");

x.setAttribute("value", '<%- ESAPI.encoder().encodeForJS(companyName) %>');

var form1 = document.forms[0];

form1.appendChild(x);

Rule 3: Avoid execution subcontexts.

I recommend that you don’t insert untrusted data into event handlers and JavaScript subcontexts. OWASP just says to be very careful, but why take the risk? You should find another way to do what you want instead. The JavaScript interpreter works a bit differently and often doesn’t stop attacks within the context. For various examples on how this can fail, you can look at the OWASP DOM XSS Cheat Sheet Rule 3.14

Rule 4: Do not apply CSS encoding in style context.

When manipulating style with JavaScript and untrusted data, you don’t have to worry about breaking out of the CSS context. Even though you don’t have to CSS encode the data beforehand, you still need to worry about unsafe CSS properties. Remember that you should never let unsafe data specify which property gets changed. It’s always better to use a whitelist instead. You should also make sure URLs don’t have execution context within them, and don’t accept expression or such values in the property.

Rule 5: JavaScript and URL encode when creating links.

This rule is similar to rule 5 for regular XSS attacks but also includes the JavaScript encoding layer:

14. https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet#RULE_.233_-_Be_Careful_when_Insert-ing_Untrusted_Data_into_the_Event_Handler_and_JavaScript_code_Subcontexts_within_an_Execution_Context

Chapter 11. Fight Cross-Site Scripts

158

<%

var urlEncoded = ESAPI.encoder().encodeForURL(userRelativePath);

var jsEncoded = ESAPI.encoder().encodeForJS(urlEncoded);

%>

var href = '<%- jsEncoded %>';

var x = document.createElement("a");

x.setAttribute("href", href);

var y = document.createTextElement("Click Me To Test");

x.appendChild(y);

document.body.appendChild(x);

In document Secure Your Node.js Web Application (Page 163-168)