Security Best Practices in JavaScript
Security means protecting your app and your users from bad actors. Think of it like locking your house — you would not leave your front door wide open. The same logic applies to web applications.
JavaScript runs directly in the browser. It has access to the DOM, cookies, localStorage, and user input. That makes it a common target for attacks. If you are not careful with how you handle data, attackers can take advantage of that.
Here are the main types of threats we will cover in this tutorial:
- XSS (Cross-Site Scripting) — injecting malicious scripts into your page
- Injection Attacks — executing attacker-controlled code or commands
- Stolen Tokens — stealing authentication tokens to impersonate users
- Insecure Communication — sending sensitive data over unencrypted connections
Let us start with the most fundamental rule: never trust user input. Here is a simple example of a dangerous vs. safe approach:
// DANGEROUS: directly injecting user input into the DOM
const userInput = document.querySelector("#search").value;
document.getElementById("result").innerHTML = userInput; // never do this!
// SAFER: use textContent instead
document.getElementById("result").textContent = userInput;
The innerHTML property parses the string as HTML — which means any script tags or event handlers in the user's input will actually run. textContent treats everything as plain text, so it is safe.
XSS happens when an attacker injects malicious scripts into a web page that other users then unknowingly run. Imagine someone slipping a fake note into a public bulletin board — other people read it thinking it is real, and act on it.
There are two common types of XSS:
- Stored XSS — the attacker's script is saved to a database and served to all users who visit the page
- Reflected XSS — the malicious script is embedded in a URL and executed when the victim clicks the link
The most common cause of XSS in JavaScript is using innerHTML with user-provided data:
// Attacker enters: <script>alert('Hacked!')</script>
// If you do this, it runs the attacker's script:
element.innerHTML = userInput; // DANGEROUS
// Safe alternatives:
element.textContent = userInput; // strips HTML tags completely
element.innerText = userInput; // also safe
Tip: Never use innerHTML with user-provided data. Use textContent whenever you only need to display text.
Sometimes you genuinely need to render HTML (for example, a rich text editor). In that case, use a trusted sanitization library like DOMPurify to clean the input before inserting it:
// Using DOMPurify to sanitize HTML (safe when you must render HTML)
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;
DOMPurify strips out any dangerous tags and attributes while keeping the safe formatting intact.
An injection attack is when an attacker sneaks their own commands into your application. The most famous example is SQL Injection on the backend. In JavaScript, the equivalent danger is eval() injection and DOM-based injection.
eval() takes a string and executes it as JavaScript code. If that string comes from user input, the attacker can run anything they want:
// DANGEROUS: eval runs whatever string you give it
const userCode = "alert('Injected!')";
eval(userCode); // never do this with user input!
// SAFE: avoid eval() entirely
// Use JSON.parse() for data parsing
const data = JSON.parse(userInput); // safe for parsing JSON
The same problem can appear with template literals when you build HTML by concatenating user input and then passing it to innerHTML:
// DANGEROUS: building HTML with template literals + user input
const html = `<div>${userInput}</div>`;
element.innerHTML = html; // attacker can inject HTML/JS
// SAFE approach:
const div = document.createElement('div');
div.textContent = userInput; // only text, no HTML parsing
element.appendChild(div);
Rule of thumb: Never execute or render user input directly without sanitizing it first. Avoid eval() entirely — there is almost always a better alternative.
Authentication is about proving who you are. A common approach is token-based authentication — after you log in, the server gives you a token (often a JWT — JSON Web Token) that you include with future requests to prove your identity.
One of the most important security decisions you will make is where to store that token. Many developers reach for localStorage because it is easy — but it has a serious security flaw:
// ❌ Storing in localStorage (accessible to JS, vulnerable to XSS)
localStorage.setItem('token', jwtToken);
// ✅ Better: Use HttpOnly cookies (not accessible via JS at all)
// This is set by the server, not by your JS code:
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
An HttpOnly cookie cannot be read by JavaScript at all. That means even if an attacker manages to inject a script via XSS, they cannot steal the token — it is invisible to JavaScript. Always use HTTPS when transmitting tokens so they cannot be intercepted in transit.
Another common mistake is accidentally logging sensitive data to the console:
// ❌ Never do this — logs are visible to anyone with DevTools
console.log("User token:", userToken);
console.log("Password:", password);
// ✅ Remove all sensitive console.logs before deploying
Console logs are visible to anyone who opens DevTools in their browser. Always clean up sensitive logs before deploying to production.
HTTPS encrypts the data travelling between the browser and the server. Nobody in between — not your ISP, not someone on the same Wi-Fi — can read or tamper with it. Think of HTTP as sending a postcard (anyone who handles it can read it) and HTTPS as sending a sealed envelope.
In JavaScript, always use https:// URLs when making API calls:
// ❌ Insecure
fetch("http://api.example.com/data");
// ✅ Secure
fetch("https://api.example.com/data");
Beyond HTTPS, there are a few key HTTP security headers you should know about:
- Content Security Policy (CSP) — tells the browser which sources scripts are allowed to load from. This drastically reduces the damage XSS can do.
- CORS (Cross-Origin Resource Sharing) — controls which external domains are allowed to make requests to your API. Never set Access-Control-Allow-Origin: * in production — that allows any website to access your API.
Never trust user input — always validate and sanitize it before using it. Think of it like checking ID at a club entrance. You verify before letting anyone in, no exceptions.
There are two levels of validation: client-side (in the browser) and server-side (on the backend). Both matter, but for different reasons:
- Client-side validation — fast feedback for the user, improves UX, but can be bypassed by anyone who opens DevTools
- Server-side validation — the real security check. Always required because client-side checks are optional from the server's perspective
// Client-side validation example
function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
const email = document.querySelector('#email').value;
if (!isValidEmail(email)) {
alert("Please enter a valid email address.");
}
You should also sanitize input before storing or displaying it — strip out any characters that could be interpreted as HTML or code:
// Remove dangerous characters from a string
function sanitize(input) {
return input.replace(/[<>"'&]/g, '');
}
const cleanInput = sanitize(userInput);
When including user input in URLs, use encodeURIComponent() to prevent URL injection:
// Safely include user input in a URL
const searchTerm = encodeURIComponent(userInput);
const url = `https://example.com/search?q=${searchTerm}`;
- Security in JavaScript is about protecting users and apps from malicious code, data theft, and attacks
- XSS happens when attacker scripts get injected into your page — use textContent instead of innerHTML to avoid it
- Avoid eval() with user input — it can execute arbitrary code
- Store authentication tokens in HttpOnly cookies, not localStorage, to protect them from XSS
- Always use HTTPS for API calls and sensitive data transmission
- Validate input on the client for UX, but always validate on the server for real security
- Use encodeURIComponent() to safely include user data in URLs
- Never log sensitive data (tokens, passwords) to the console in production code
- What is Cross-Site Scripting (XSS), and how can you prevent it in JavaScript?
- Why is using innerHTML with user input dangerous?
- What is the difference between innerHTML and textContent?
- Why is eval() considered a security risk?
- Where should you store authentication tokens — localStorage or HttpOnly cookies? Why?
- What is the difference between HTTP and HTTPS?
- What is a Content Security Policy (CSP) and what problem does it solve?
- Why is client-side validation alone not enough for security?
- What does encodeURIComponent() do and when should you use it?
- What is CORS and why does it exist?
- Why should you never log tokens or passwords to the console?