Security Best Practices in JavaScript

Why should you care about Security Best Practices in JavaScript?

Small security mistakes can expose user data and damage trust very quickly. Learning safe defaults now helps you build apps that are harder to break and prepares you for real world backend and frontend work.

Web security protects your application and its users from attacks, unauthorized access, and data theft. JavaScript security is one part of that work: the browser must handle untrusted data safely, and the server must enforce access and data rules.

JavaScript can read page content, respond to user input, and make network requests. Those features are useful, but unsafe code can let an attacker run unwanted code in a user's browser or expose sensitive data.

Here are the main types of threats we will cover in this tutorial:

  • XSS (Cross-Site Scripting) - unwanted JavaScript runs in another user's page
  • Injection - attacker-controlled data is treated as code or commands
  • Authentication and authorization mistakes - the wrong person can access an account or action
  • Insecure communication - sensitive data travels without adequate protection

Let us start with the most fundamental rule: never trust user input. Here is a simple example of a dangerous vs. safe approach:

javascript

// 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 a string as HTML, so attacker-controlled markup can create dangerous elements or event-handler attributes. textContent displays the string as text instead. It does not remove tags from the original string; it simply does not interpret them as markup in that DOM element.

A browser is not a trusted place to enforce business rules. Users can edit requests, skip form checks, and inspect the JavaScript that you send to them. Treat browser input, URL values, and API responses as data that may be controlled by someone else.

Good security uses layers. Safe DOM APIs reduce XSS risk, HTTPS protects data while it travels, secure authentication limits account access, and server-side validation makes the final decision. No single JavaScript function replaces those layers.

Cross-Site Scripting (XSS) is a security problem where an attacker gets unwanted JavaScript or markup into another user's page. The browser then treats the attacker's input as part of the application.

The three common forms are:

  • Stored XSS - dangerous input is saved, such as in a comment, and shown to later visitors.
  • Reflected XSS - dangerous input arrives in a request, often a link, and is immediately placed into the response.
  • DOM-based XSS - browser JavaScript reads attacker-controlled data, such as a URL hash, and sends it to an unsafe DOM sink.

This example is intentionally unsafe. Do not use it in production:

javascript

// Attacker-controlled input can contain markup or event attributes.
element.innerHTML = userInput; // DANGEROUS

// Safe alternatives:
element.textContent = userInput; // displays the input as text
element.innerText = userInput;   // also treats it as rendered text

For plain text, use textContent. If the application must render user-provided HTML, use a maintained sanitizer such as DOMPurify and configure it for that specific need. A Content Security Policy (CSP) can reduce the impact of mistakes, but it does not make unsafe HTML safe by itself.

A DOM-based example often looks like this:

javascript

// DANGEROUS: the URL hash is attacker-controlled.
const message = document.querySelector("#message");
message.innerHTML = window.location.hash.slice(1);

The problem is the unsafe destination, called a sink. The source may be a form, URL, database response, or API response. Track untrusted data and choose a safe output method for its destination.

Sometimes you genuinely need to render HTML, such as in a rich-text editor. In that case, sanitize it before inserting it:

javascript

// DOMPurify must be loaded separately; it is not built into JavaScript.
// Using DOMPurify to sanitize HTML when rich HTML is required:
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. SQL Injection is usually server-side, while XSS is browser-side and happens when untrusted input is treated as executable HTML/JavaScript in the page. In JavaScript, common risks include eval() injection and unsafe DOM sinks.

eval() takes a string and executes it as JavaScript code. If that string comes from user input, the attacker can run anything they want:

javascript

// 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:

javascript

// Example assumes #output exists and #comment contains user input.
const element = document.querySelector("#output");
const userInput = document.querySelector("#comment").value;

// 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 proper output handling. Avoid eval() entirely - there is almost always a better alternative.

Authentication checks who a user is. Authorization checks what that user is allowed to do. A login can succeed while authorization still blocks a user from another user's data.

With token-based authentication, the server gives the browser a credential after login. An access token is a short string used to identify that session. A JWT (JSON Web Token) is one token format; using JWT does not automatically make an application secure.

Where a token is stored depends on the application's architecture and threat model. localStorage is readable by JavaScript, so an XSS bug can expose a token:

javascript

const jwtToken = "fake-token-for-example";

// Risky when this token grants access: JavaScript can read it.
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

HttpOnly prevents JavaScript from directly reading a cookie. Secure sends it only over HTTPS, and SameSite limits when the browser sends it with cross-site requests. These settings reduce risk, but they do not remove every XSS risk and do not replace server-side authorization.

Cross-Site Request Forgery (CSRF) tricks a browser that already has a session cookie into sending an unwanted state-changing request. Cookie-based authentication needs appropriate defenses, such as SameSite settings, CSRF tokens, and Origin checks.

Another common mistake is accidentally logging sensitive data to the console:

javascript

const userToken = "fake-token-for-example";
const password = "fake-password-for-example";

// 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 data in transit between the browser and the server, and helps protect integrity and authenticity with TLS certificates. It protects against eavesdropping and many tampering attacks on the network path, but HTTPS alone does not secure your whole application.

Use HTTPS for network requests that cross the network. Same-origin relative URLs such as /api/data inherit the page's secure origin, while an explicit cross-origin URL should use https://:

javascript

// ? 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 can reduce XSS impact.
  • HTTP Strict Transport Security (HSTS) - tells browsers to use HTTPS only for your site.
  • X-Content-Type-Options - helps prevent MIME type sniffing.
  • Referrer-Policy - controls how much referrer information is sent.
  • Permissions-Policy - limits access to sensitive browser features.
  • CORS (Cross-Origin Resource Sharing) - controls whether browser JavaScript is allowed to read cross-origin responses. It does not stop requests from being sent.
  • Access-Control-Allow-Origin: * can be acceptable for some truly public resources, but it cannot be used with credentialed requests (such as cookies).
Note: These headers are set on the server side, but as a JavaScript developer you should understand why they matter and be able to discuss them in interviews.

Never trust user input. Validate important data and handle its output safely before using it. Validation checks whether data follows expected rules; output handling decides how that data is displayed or sent to another system.

There are two levels of validation: client-side validation in the browser and server-side validation on the backend. Client-side checks give quick feedback and improve the user experience. Server-side checks enforce the rules because a user can bypass browser code and send a request directly.

  • 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

javascript

// 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.");
}

This email check only checks a basic shape. It cannot prove that an address exists or can receive mail. The server must validate the value again and apply the application's actual rules.

If you must allow user-provided HTML, do not rely on a homemade regular expression. Use a well-maintained HTML sanitizer such as DOMPurify. When including user input as a URL component, encodeURIComponent() is useful for encoding that component, but it is not a universal defense for every URL or injection problem.

javascript

// Safely include user input in a URL
const searchTerm = encodeURIComponent(userInput);
const url = `https://example.com/search?q=${searchTerm}`;

Use this review pattern whenever browser code handles untrusted input: identify the dangerous sink, replace it with a safer API, then add browser and server defenses.

Concrete browser-side mitigations for common mistakes.
Risky patternSafer patternBrowser mitigation
User input into innerHTMLtextContent or DOM node APIsUse a CSP and sanitize with a maintained library only when HTML is required.
eval(userInput)Parse structured data with JSON.parseUse CSP to reduce impact, but remove the code execution sink.
Token in localStorageServer-set HttpOnly; Secure; SameSite cookieUse CSRF tokens or Origin checks for state-changing requests.
HTTP API URLHTTPS or same-origin relative URLUse HSTS and avoid mixed content.
Trusting browser validationValidate and authorize again on the serverTreat every browser value as attacker-controlled.

javascript

const message = document.querySelector("#message");
const input = document.querySelector("#comment").value;

// Vulnerable: parses attacker-controlled HTML.
message.innerHTML = input;

// Secure for plain text: never parses the input as markup.
message.textContent = input;

// If rich HTML is required, sanitize with a maintained library
// before assigning to innerHTML, and keep a restrictive CSP.

Security is layered: safe DOM APIs reduce XSS, CSP limits script sources, HTTPS protects data in transit, cookie attributes reduce token exposure, CSRF defenses protect state-changing requests, and server-side validation remains authoritative.

Never test with real secrets: use fake tokens and sample input, and do not paste credentials into browser consoles or online tools.
  • Prefer textContent when displaying untrusted plain text.
  • Avoid eval() with untrusted input.
  • Sanitize untrusted HTML when HTML rendering is required.
  • Use HTTPS and configure appropriate security headers.
  • Validate and authorize requests on the server.
  • Protect authentication tokens and use appropriate CSRF defenses.
  • Never expose passwords, tokens, or API secrets in client-side code.
  • Avoid logging sensitive information and keep dependencies updated.
  • Security in JavaScript is about protecting users and apps from malicious code, data theft, and attacks
  • XSS happens when attacker-controlled HTML is inserted into unsafe sinks like innerHTML; use textContent when you only need text
  • Avoid eval() with user input - it can execute arbitrary code
  • HttpOnly cookies can reduce JavaScript token access, but still require Secure, SameSite, and CSRF protections
  • 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
  • Keep dependencies updated and review third-party packages because they are part of your app's supply chain
  • Watch for prototype pollution when merging untrusted object data; polluted prototypes can change app behavior in surprising ways
  • Never log sensitive data (tokens, passwords) to the console in production code
What's next? Continue to the Testing JavaScript tutorial to learn how to write reliable, bug-free code.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.