Skip to Content
Course content

165: Safe Handling of User Input

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a snippet of code that looks perfectly innocent. I've seen this exact pattern in dozens of junior developers' PRs, and it's one of the most dangerous mistakes you can make in a front-end application.

// A simple script to greet a user based on a URL parameter
const params = new URLSearchParams(window.location.search);
const name = params.get('name');

const welcomeDisplay = document.getElementById('welcome-message');
welcomeDisplay.innerHTML = `<strong>Welcome, ${name}!</strong>`;

At first glance, it works. If the URL is ?name=Alice, the page says Welcome, Alice!. But here is where things go south. What happens if a user (or a malicious actor) sends a link to someone else that looks like this?

?name=<img src=x onerror="alert('Your session cookie is: ' + document.cookie)">

Because we used .innerHTML, the browser doesn't just see a name; it sees an HTML img tag with a broken source. When that image fails to load, the onerror event fires, executing whatever JavaScript was inside it. This is a classic Cross-Site Scripting (XSS) attack. I've seen this used to steal session tokens, redirect users to phishing sites, or deface pages.

The Danger of Implicit Trust

The root of the problem is that .innerHTML tells the browser: "Take this string and parse it as actual HTML." When you mix that with data coming from a user—whether it's from a URL, a form input, or an API—you are essentially giving the user permission to write code directly into your page.

You might be tempted to try and "clean" the string using a regular expression to remove <script> tags. Don't do that. Attackers are incredibly creative; they can use different encodings, case-mixing (<sCrIpT>), or event handlers like onmouseover to bypass your filters. Trying to blacklist "bad" characters is a losing game.

Neutralizing Input with textContent

The simplest and most effective fix for the example above is to stop treating user input as HTML. If you just want to display text, use .textContent.

const params = new URLSearchParams(window.location.search);
const name = params.get('name');

const welcomeDisplay = document.getElementById('welcome-message');
// Create a strong element separately
const strongElement = document.createElement('strong');
strongElement.textContent = `Welcome, ${name}!`;

welcomeDisplay.innerHTML = ''; // Clear previous content
welcomeDisplay.appendChild(strongElement);

By using .textContent, the browser treats the input as literal text. If someone passes in <img src=x...>, the page will literally display those characters on the screen rather than executing them. It's a complete neutralization of the threat.

Handling Necessary HTML with Sanitization

Now, I know what you're thinking: "What if I actually need to allow some HTML?" Maybe you're building a blog editor that allows <b> and <i> tags. In those cases, you cannot use .textContent, and you absolutely cannot trust your own regex.

This is where you use a dedicated sanitization library. The industry standard is DOMPurify. It doesn't just look for "bad" words; it parses the HTML into a DOM tree, strips out everything that isn't on a strict "allow-list," and returns a clean string.

import DOMPurify from 'dompurify';

const rawUserInput = params.get('bio'); 
// User might have sent: <img src=x onerror=alert(1)> Hello!
const cleanHTML = DOMPurify.sanitize(rawUserInput);

welcomeDisplay.innerHTML = cleanHTML; 
// Now it only contains the safe parts of the input.

My rule of thumb: Default to .textContent. If you absolutely must render HTML from a user, use a vetted library. Never try to write your own "safe" HTML parser.




📋 Practical Task

Secure the User Profile Bio Page

You are working on a profile page where users can set a "Bio." The current implementation is vulnerable to XSS. Your task is to fix the code so that the bio is displayed safely, but the "Last Updated" timestamp remains bolded.

Requirements:

  • Ensure that any HTML tags provided in the userBio variable are rendered as literal text (not executed).
  • The timestamp must still be rendered inside a <strong> tag.
  • Do not use .innerHTML to insert the userBio.
<div id="profile-container"></div>

<script>
const userBio = "<img src=x onerror='alert(\"Hacked!\")'> I love JavaScript!";
const timestamp = "Updated 2 mins ago";

const container = document.getElementById('profile-container');

// FIX THIS PART:
// Currently: container.innerHTML = `<p>${userBio}</p> <strong>${timestamp}</strong>`;
// Your code here:
</script>
Rating
0 0

There are no comments for now.

to be the first to leave a comment.