How to Use innerHTML in JavaScript, Plus Alternate Methods
If you’re creating any dynamic web page these days, making sure you have relevant and up-to-date information is vital.
Whether it’s showing stock quotes, the current time, the user’s name, or any other piece of info that changes with time or user, you’ll want to make sure you’re regularly updating this dynamic content to produce the best experience for your users.
Luckily for you, we can associate these dynamic pieces of information with HTML elements and easily display their current state using the innerHTML property.
This blog post will walk you through the pros and cons of using the innerHTML
property and how you can use it safely without opening the doors to potential Cross-Site scripting (XSS) attacks.
What is innerHTML in JavaScript?
innerHTML
is an HTML element property that has two uses for web developers:
1) You can use it to get the internal HTML content of any HTML element as an HTML string.
2) You can also use it to set or change elements’ innerHTML
content.
Take the following HTML example:
<div id=”someDivElement”>
<span>Hello World</span>
</div>
Code language: HTML, XML (xml)
To access the innerHTML you’ll first need to access the element itself with getElementById()
:
const someDivElement = document.getElementById(“someDivElement”);
Then we can access the innerHTML
:
console.log(someDivElement.innerHTML);
This will give us "<span>Hello World</span>"
as a string. And if you want to modify the innerHTML
property of the element, it can be achieved in the following way:
someDivElement.innerHTML = “<span>Something just like this…</span>”;
Here’s an example that can be used by to-do apps where list items are appended to an existing list:
const todos = [
"Exercise",
"Have milk",
"Buy bread",
"Walk the dog",
"Sleep timely :)"
];
const todoListPreview = document.getElementById(“tdlst-preview”); /* returns an <ol> */ element
todos.forEach((todo) => {
todoListPreview.innerHTML += `<li>${todo}</li>`;
});
Code language: JavaScript (javascript)
This will add all the to-do items as <li>
tags in our ordered list element. These are the ways we can get and set the innerHTML
properties.
However – as with any piece of code – there can be exceptions. A common one you may see with innerHTML
is theSyntaxError
, which is thrown when the provided HTML string is ill-formed. Here’s an example:
someDivElement.innerHTML = “<span>William Bradley "Brad" Pitt</span>”
Code language: HTML, XML (xml)
The moment the browser parses this script, "Brad"
is perceived as an unknown identifier because of the double quotes, and the SyntaxError
exception is thrown.
When not to use innerHTML
Using innerHTML
is fine if you’re using it to get the value of an element’s innerHTML
content. Things change, however, if it’s used to set values, as it accepts all HTML tags, including the script
tag. This means you could potentially open up a portal to your cookies and users’ personal information via a Cross Site Scripting (XSS) attack.
An XSS attack is a type of web security vulnerability that allows an attacker to inject malicious code into a webpage. Then that code is executed by the web browser when an unsuspecting user visits the page.
Take the example below that allows a bad script to send a user’s cookies to its server, which then can be used to impersonate the actual user and perform malicious actions:
document.getElementById(“someLogoutButton”).innerHTML = “<script>callHome(document.cookie);</script>”;
Code language: HTML, XML (xml)
Fortunately, the World Wide Web Consortium (W3C) has since released a standard on dynamic markup insertion in HTMLthat states that script
elements are not executed when inserted using innerHTML
. However, hackers have found other ways to carry out XSS attacks.
Let’s take the following example of the img
tag. It has an onerror
attribute that accepts JavaScript and is allowed to be run.
<img src=123 onerror=alert(“Haha!”)>
Once browsers parse this tag, the value of src
is invalid as browsers expect a URL. Thus, an error will be thrown, and as the image tag has an onerror
listener, the script inside will be executed.
How to use innerHTML without creating XSS vulnerabilities
To prevent XSS issues, you always want to sanitize your user input, especially when it’s going to be rendered as-is.
Say you’re building a WYSIWYG editor in which you’re letting users save their HTML content in your database via innerHTML. Since you can’t trust your users’ input, you’ll need to implement some sanitization to prevent them from adding malicious code.
There are a lot of open-source libraries available that provide this sanitization service. One of my favorites is sanitize-HTML, which provides HTML cleaning for browser and node environments.
npm install sanitize-HTML
The library is fairly easy to use. Here are a few examples:
// the es module way
import sanitizeHtml from 'sanitize-html';
// the commonjs way
const sanitizeHtml = require('sanitize-html');
sanitizeHtml('<img src=x onerror=alert(1)//>'); // returns ""
sanitizeHtml('<svg><g/onload=alert(2)//<p>'); // returns ""
sanitizeHtml('<p>abc<iframe//src=jAva&Tab);script:alert(3)>def</p>'); // returns <p>abcdef</p>
sanitizeHtml('<TABLE><tr><td>HELLO</tr></TABL>'); // returns "<table><tr><td>HELLO</td></tr></table>"
sanitizeHtml('<UL><li><A HREF=//google.com>click</UL>'); // returns "<ul><li><a href=\"//google.com\">click</a></li></ul>"
Code language: JavaScript (javascript)
NOTE: You can try the library out at RunKit.
By default, sanitize-HTML has a predefined set of rules on what tags and attributes to allow and remove/escape; however, it can be tailored (refer to the documentation) further per the developers’ requirements.
⚠️ Always sanitize HTML string inputs on both the front and back end to reduce the attack vector substantially.
innerHTML vs. createElement
createElement is an alternative method of innerHTML, so here we’ll look at the two.
createElement
is faster, as browsers are not required to parse the HTML string and then build a node tree out of it; it also doesn’t have to attach event listeners as innerHTML
does. Using innerHTML
will cause browsers to reparse and recreate all DOM nodes inside the element whose innerHTML
is modified.
However, if you’re writing a dynamic solution like a Markdown to HTML converter with a real-time preview, then innerHTML
is the way to go as it is a “one-size-fits-all” approach. This is precisely what’s required for this particular conversion and real-time preview. Building the same logic with createElement
would be a pain and render highly coupled logic with unextendable code.
We can see this if we take the same to-do list example discussed above but use createElement
instead of innerHTML
:
const todos = [
"Exercise",
"Have milk",
"Buy bread",
"Walk the dog",
"Sleep timely :)"
];
const todoListPreview = document.getElementById(“tdlst-preview”); // returns an <ol> element
todos.forEach((todo) => {
todoListPreview.innerHTML += `<li>${todo}</li>`;
const listItemElement = document.createElement(“li”);
listItemElement.textContent = todo;
todoListPreview.appendchild(listItemElement);
});
Code language: JavaScript (javascript)
This will work for this particular problem, but it’s not a scalable approach.
You’d be adding a lot of work for yourself and your team if, for example, you wanted to build a Rich Text Editor where you have to write separate logic for each of the new functionalities you might add.
Using innerHTML with a simple HTML sanitizer is a much more efficient and practical approach.
Conclusion
In HTML, assigning a string to the innerHTML
property is okay in some situations. However, if you can’t be sure of what the string contains—for example, if a user provides it and it is possibly malicious—it’s best to use createElement
or sanitize the input before storing it in a database.
The open-source “sanitization” libraries strip off specific tags and attributes to make the desired HTML string XSS proof. Sanitization should be done on both the frontend and backend as a best practice to help reduce the risk of XSS attacks. When implementing sanitization on the frontend, it should be done at the time of render when the user provides input. And, on the backend, it should be done before storage in the database.
This post was written by Keshav Malik. Keshav is a full-time developer who loves to build and break stuff. He is constantly looking for new and exciting technologies and enjoys working with diverse technologies in his spare time. He loves music and plays badminton whenever the opportunity presents itself.