Complete JavaScript Course Module 16: Advanced: Professional JavaScript

JavaScript Security Basics: XSS, innerHTML and Safe Code

  • Beginner
  • Lesson 16.2
  • 5 min read
  • 72 of 76 in the course

In short

JavaScript security basics help you protect users. Learn what XSS is, why textContent is safer than innerHTML, why to avoid eval, and how to handle secrets and links safely. What is XSS (cross-site scripting)?

JavaScript security basics help you protect users. Learn what XSS is, why textContent is safer than innerHTML, why to avoid eval, and how to handle secrets and links safely.

What is XSS (cross-site scripting)?

XSS (cross-site scripting) is when an attacker gets their own code to run on your website, inside other people's browsers. It is one of the most common web security problems.

Imagine a comment section. A normal user writes "Great post!". An attacker writes a "comment" that contains HTML with a script hidden inside it. If your page puts that text into the page as HTML, the browser runs the attacker's code for every visitor. That code could steal login sessions, change the page, or send fake requests as the user.

The root cause is always the same: the page treats user data as code.

textContent vs innerHTML: which is safe?

innerHTML reads the string as HTML and builds elements from it. textContent puts the string in as plain text, so any tags appear as visible characters and never run.

<form id="commentForm">
  <input id="commentInput" placeholder="Write a comment" required>
  <button>Post</button>
</form>
<ul id="comments"></ul>

<script>
  const form = document.querySelector('#commentForm');
  const list = document.querySelector('#comments');

  form.addEventListener('submit', (event) => {
    event.preventDefault();
    const text = document.querySelector('#commentInput').value;

    // UNSAFE: list.innerHTML += `<li>${text}</li>`;

    // SAFE: build the element and set its text
    const li = document.createElement('li');
    li.textContent = text;
    list.append(li);

    form.reset();
  });
</script>

Try posting <img src=x onerror="alert('hacked')">. With the safe version, you see that text exactly as typed in the list. With the unsafe innerHTML version, the browser would create an image, fail to load it, and run the alert. A real attacker would run something much worse.

Common mistake: Thinking "the <script> tag is blocked, so innerHTML is safe". Browsers do not run <script> tags inserted with innerHTML, but attackers use event attributes like onerror instead. Never put untrusted text into innerHTML.

When you need some HTML

Sometimes you want a template with structure. Use innerHTML only for your own fixed markup, and fill in user data with textContent.

const renderUserCard = (user) => {
  const card = document.createElement('article');
  card.className = 'user-card';
  card.innerHTML = '<h3></h3><p class="bio"></p>';   // fixed markup, no user data

  card.querySelector('h3').textContent = user.name;
  card.querySelector('.bio').textContent = user.bio;
  return card;
};

document.body.append(renderUserCard({ name: 'Ana', bio: '<b>Loves</b> JavaScript' }));
// You will see: Ana, and the bio shown literally as "<b>Loves</b> JavaScript"

If you truly must show user-written HTML (like a rich text editor), clean it with a trusted library such as DOMPurify first. The same danger applies to insertAdjacentHTML() and outerHTML.

Never use eval (and its friends)

eval(string) runs any string as JavaScript code. If any part of that string comes from a user, the URL, or a server, an attacker can run whatever they want. The same is true for new Function(string) and passing a string to setTimeout('code', 1000).

// Dangerous: turning text into code
// const result = eval(userInput);

// Safe alternatives
const data = JSON.parse('{"name":"Ana","age":28}');   // parse data, not code
console.log(data.name);                                // Output: Ana

const actions = {
  greet: () => console.log('Hello!'),
  bye: () => console.log('Goodbye!'),
};
const choice = 'greet';            // imagine this came from a button
actions[choice]?.();               // Output: Hello!

Use JSON.parse() for data and an object of functions instead of building code from strings. You almost never need eval.

Validate on the server and keep secrets out of front-end code

Front-end validation is not security

Form checks in JavaScript are for a nice user experience. They are not protection. Anyone can open DevTools, turn off your JavaScript, or send a request directly with other tools. The server must check every piece of data again: type, length, format and permissions.

Never put secrets in front-end code

Everything you send to the browser can be read: your JavaScript files, localStorage, network requests. So never put these in front-end code:

  • Secret API keys (payment, email or AI service keys)
  • Database passwords
  • Admin passwords or "hidden" admin checks like if (user === 'admin')

Instead, keep secret keys on your own server. Your front end calls your server, and your server calls the other service with the key. Some keys are designed to be public (like a map widget key that is locked to your domain); the service's documentation will say so.

Note: Hiding a key in a .env file does not help if a build tool copies it into your JavaScript bundle. If the browser can use it, users can read it.

When you open a link in a new tab with target="_blank", add rel="noopener noreferrer". noopener stops the new page from controlling your page through window.opener. Modern browsers already do this by default for target="_blank", but adding it is clear and safe. When you build links from user data, also check the address starts with https:// so nobody can sneak in a javascript: link.

const addSafeLink = (url, label) => {
  const parsed = new URL(url, location.href);
  if (!['https:', 'http:'].includes(parsed.protocol)) {
    console.warn('Blocked unsafe link:', url);
    return;
  }
  const a = document.createElement('a');
  a.href = parsed.href;
  a.textContent = label;
  a.target = '_blank';
  a.rel = 'noopener noreferrer';
  document.body.append(a);
};

addSafeLink('https://developer.mozilla.org', 'MDN');   // added
addSafeLink('javascript:alert(1)', 'Click me');        // Output: Blocked unsafe link: javascript:alert(1)

Content Security Policy (CSP) is an extra safety net that your server sends as an HTTP header. It tells the browser which sources of scripts, styles and images are allowed, for example "only scripts from my own domain, and no inline scripts". If an attacker manages to inject a script anyway, the browser refuses to run it. CSP does not replace safe coding, but it greatly limits the damage of a mistake. You can learn how to write one on MDN's Content Security Policy guide.

Practice exercise

Build a small "guestbook": a form with a name and a message, and a list that shows each entry as "Name: message". Make it safe against XSS by using createElement and textContent only. Then test it by entering <b>bold</b> and <img src=x onerror=alert(1)> and check that they appear as plain text.

Hint: create a <strong> for the name and a text node or <span> for the message, and never use innerHTML with the input values.

Key takeaways

  • XSS happens when user data is treated as code; it can steal sessions and change your page.
  • Use textContent and DOM methods for user data; use innerHTML only for your own fixed markup.
  • Never use eval, new Function or string timers; use JSON.parse and objects of functions.
  • Front-end validation is for user experience; the server must validate everything again.
  • Never put secrets in front-end code, use rel="noopener noreferrer" for new-tab links, and add CSP as a safety net.