UI Development

Safe Ways To Inject HTML Through JavaScript

Introduction

This blog is about the safest techniques used to inject HTML markup through JavaScript on the web sites.

Why should we follow safety while injecting HTML?

When we inject HTML using javascript without sanitizing the code can cause cross site scripting (XSS) which is vulnerable. Injecting the markup using javascript brings a lot of possibilities for a malicious user to modify the website, retrieve the sensitive information using cookies. Therefore, this can bring some serious website damages, information leakage and even hack the website. JavaScript can send HTTP requests with arbitrary content to arbitrary destinations by using XMLHttpRequest and other mechanisms.

We must be cautious about the following consequences of malicious javascript:-

  • Cookie theft: The attacker can access the user sensitive data stored cookies associated with the website using document.cookie, send them to his own server, and use them to extract sensitive information like session IDs.

  • Key logging: The attacker can register a keyboard event listener using addEventListener and then send all the user’s keystrokes to his own server, potentially recording sensitive information such as passwords and credit card numbers.

  • Phishing: The attacker can insert a fake login form into the page using DOM manipulation, set the form’s action attribute to target his own server, and then trick the user into submitting sensitive information.

Is innerHTML Safe?

The simplest way to inject the markup through javascript is achieved using innerHTML property because it provides a really convenient way to create HTML templates as strings and inject them into the DOM tress. But this can cause the code to expose for cross-site scripting attacks which is very dangerous.

Eample to inject code with innerHTML:

Another approach using template literals

When we use innerHTML there is a high risk of XSS attack because the malicious code would get injected into the website and gets executed. This happens because innerHTML renders complete markup and not just text through js.

There is an alternate solution though. Just injecting a script tag element won’t expose the code to get attacked, because the section of the DOM that is getting injected has been parsed and run.

JavaScript that runs at a later time, though, will.

The markup written above is with the minimal danger. There will be a high risk when the content generated by user or data from third party API’s or some external source which is not controlled by a developer.

How about document.write?

All the script references should be moved to the bottom of the page because of document.write. When the browser hits a script block or reference it stops everything till the script is load, parsed and executed because it assumes that document.write is used in the script
When document.write is called the DOM is manipulated in a way that forces the browser to completely reload the markup and re-render it by wiping out the rendered data and replace it from the beginning.
This causes a noticeable flash during a page load and takes longer time for loading the content in the page compared to without using document.write. Due to this reason we should avoid using document.write in the scripts.

Better ways than innerHTML and document.write

  • document.createElement()
    In HTML document, the document.createElement() is a method used to create the HTML element. The element specified using elementName is created or an unknown HTML element is created if the specified elementName is not recognized.
    Syntax:

    It generates on demand HTML markup without re-rendering and refreshing the whole page as innerHTML does. It pretends to be a separate page but renders the content on a single page. This saves a lot of both computation power and RAM, so it gets quicker results than server-side rendering (SSR).
    We have few disadvantages also with document.createElement() such as loading of the page will be slow at first go, SEO problems, and caching issues. But after the first load, it becomes very smooth and user-friendly. It gives high performance and high speed and doesn’t take much RAM to run.
    Example:

  • DocumentFragment()
    The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document. Injecting a dynamic content to the website using createDocumentFragment technique is extremely useful. It requires an element target to append the content instead of inline script. createFragment is a helper method that can be reused to insert elements or a string of markup in the DOM.
    It creates a document fragment containing the desired markup and returns that fragment to be appended on targeted element. Using element.appendChild method we can append the fragment, which accepts a document fragment and inserts it within the element.
    Ex:

    If there are multiple targets then you need to use querySelectorAll, as it returns a nodeList so you need to loop through else you can use querySelector which returns the first matching element. The result is a solution that will dynamically inject markup in the page at the desired location.

  • textContent()
    An alternate solution is to use textContent instead of innerHTML. Using textContent property we can get and inject text content only not the complete markup, from and into the DOM tree. It is great if we are adding only text, but if we are adding third-party content as part of some additional markup then this property doesn’t work.

Plugins that can help

  • DOMPurify
    It is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It is written in JavaScript and works in all modern browsers. It either uses a fall-back or simply does nothing. DOMPurify is used to sanitize the dirty HTML and prevents it from XSS attacks and returns a string with clean HTML by stripping out everything that contains dangerous HTML. We use the technologies the browser provides and turn them into an XSS filter. The faster your browser, the faster DOMPurify will be.

  • SanitizeHTML
    To make it safe, we need to sanitize the content (that is, remove disallowed markup) before injecting it to the DOM. If the third-party code is not allowed to contain any markup, we can use a helper method to remove markup from the code.

    This works by creating a temporary div and adding the content with textContent to escape any characters. It then returns them using innerHTML to prevent those escaped characters from transforming back into unescaped markup.

    If the third-party content is allowed to contain markup, a helper library like DOMPurify will remove any markup that’s not part of a secure whitelist before injecting it.

Conclusion

DOM injections and modifications are taxing. If you ever want to append the HTML markup with dynamic data you must prefer to use DocumentFragment instead of innerHTML, as it will improve the page health in various perspective. A DocumentFragment is a minimal document object that has no parent. It is used as a light-weight version of document to store well-formed or potentially non-well-formed fragments of XML.
Instead of appending the elements directly to the document when they are created, append them to the DocumentFragment instead, and finish by adding that to the DOM.
Now there’s only one big DOM change happening, and because of that the recalculation, painting and layout is absolute minimum.
We can also use JavaScript templating engines such as Handlebars.js, Mustache.js or Underscore.js. JavaScript templates can be useful if you want dynamic content. It can minimize the amount of data that’s returned to the client (e.g. data as an object or JSON instead of the entire markup) to the HTML, making sites faster and servers more responsive by lowering bandwidth and load.

References

https://github.com/cure53/DOMPurify

https://gomakethings.com/a-safer-alternative-to-innerhtml-with-vanilla-js/

https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment

https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#Examples

https://coderwall.com/p/o9ws2g/why-you-should-always-append-dom-elements-using-documentfragments

https://love2dev.com/blog/documentwrite/

About The Author