Client-Side Security Essentials
Reviewed & published by Brayan K
Client-side security is the practice of protecting browser-based JavaScript apps from attacks like cross-site scripting (XSS) and cross-site request forgery (CSRF) by sanitizing input, handling tokens safely, and layering defenses.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
While this online editor runs real JavaScript, some security examples require a browser environment. Download Node.js to run JavaScript on your computer, use your browser's Developer Console (Press F12) to test code snippets, or create a .html file with <script> tags and open it in your browser.
Master XSS prevention, CSRF protection, input sanitization, secure token handling, and defense-in-depth architecture.
What You'll Learn
- XSS prevention techniques
- CSRF token protection
- Input sanitization
- Secure token handling
- Content Security Policy
- Defense-in-depth architecture
Why Client-Side Security Matters
The browser is exposed territory. Anyone can inject HTML, override JavaScript, modify requests, steal tokens, and manipulate the DOM. The only safe approach is to treat all user-controlled input as hostile.
What Attackers Can Do
- Steal session tokens and cookies
- Hijack user accounts
- Inject fake login forms
- Execute actions as the user
- Redirect to malicious sites
- Install keyloggers
Your Defense Goals
- Prevent script injection (XSS)
- Block forged requests (CSRF)
- Sanitize all user input
- Secure token storage
- Implement CSP headers
- Build defense-in-depth
Cross-Site Scripting (XSS) — The #1 Frontend Threat
XSS occurs when malicious script executes inside your page. Even one tiny mistake can compromise your entire application.
Three Types of XSS
- Stored XSS: Malicious script saved in database, shown to all users (comments, profiles)
- Reflected XSS: Script injected via URL parameters, executed immediately
- DOM XSS: JavaScript directly puts user input into DOM without server involvement
// ❌ DANGEROUS - XSS vulnerabilities
const userInput = '<img src=x onerror="alert(document.cookie)">';
// This allows script execution!
document.getElementById("output").innerHTML = userInput;
// ✅ SAFE - Use textContent instead
document.getElementById("output").textContent = userInput;
// Renders as plain text, not HTML
// ❌ DANGEROUS - Setting href from user input
const link = document.createElement("a");
link.href = "javascript:alert('XSS')"; // Executes JS!
// ✅ SAFE - Validate URLs first
function safeURL(url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
return "about:blank"; // Safe fallback
}
link.href = safeURL(userInput);
// ❌ DANGEROUS - eval() with user input
eval(userInput); // NEVER do this!
// ❌ DANGEROUS - Dynamic script creation
const script = document.createElement("script");
script.textContent = userInput; // Can execute anything
console.log("✅ Always sanitize user input before rendering!");HTML Escaping — Your First Line of Defense
Escaping converts dangerous characters into safe HTML entities. This is essential when you must display user content.
// HTML escape function - prevents XSS
function escapeHTML(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
return String(str).replace(/[&<>"'`=\/]/g, char => escapeMap[char]);
}
// Test with malicious input
const maliciousInput = '<script>alert("XSS")</script>';
const safeOutput = escapeHTML(maliciousInput);
console.log("Original:", maliciousInput);
console.log("Escaped:", safeOutput);
// Output: <script>alert("XSS")</script>
// Safe to insert as HTML now
// element.innerHTML = safeOutput;
// Practical example: user comment
const userComment = `
<b>Bold attack</b>
<img src=x onerror=alert(1)>
Normal text here
`;
console.log("\nUser comment escaped:");
console.log(escapeHTML(userComment));DOM-Based XSS — Common in SPAs
DOM XSS happens when JavaScript reads user input (from URLs, forms, storage) and inserts it into the page without sanitization. This is extremely common in React, Vue, and Angular apps.
// DOM-based XSS - when URL params go directly to DOM
// ❌ DANGEROUS - Reading URL and inserting into DOM
const urlParams = new URLSearchParams("?name=<img src=x onerror=alert(1)>");
const name = urlParams.get("name");
// This would execute the XSS payload:
// document.body.innerHTML = "Hello, " + name;
// ✅ SAFE - Always sanitize URL parameters
function sanitizeParam(param) {
if (!param) return "";
// Use textContent approach
const div = document.createElement("div");
div.textContent = param;
return div.innerHTML; // Now escaped
}
const safeName = sanitizeParam(name);
console.log("Safe name:", safeName);
// ✅ SAFE - Use textContent for display
function displayUserName(name) {
const container = document.createElement("div");
container.textContent = "Hello, " + name;
return container.outerHTML;
}
console.log("Safe display:", displayUserName(name));
// ❌ DANGEROUS - document.write with user data
// document.write(location.hash.substring(1));
// ✅ SAFE - Validate hash before use
function safeHash() {
const hash = location.hash.substring(1);
// Only allow alphanumeric and hyphens
if (/^[a-zA-Z0-9-]+$/.test(hash)) {
return hash;
}
return "";
}
console.log("\nAlways validate URL parameters and hash fragments!");Cross-Site Request Forgery (CSRF)
CSRF forces a victim's browser to make authenticated requests without their knowledge. If your app uses cookies for authentication, you're vulnerable.
CSRF Defense Layers
- SameSite cookies: SameSite=Strict or Lax
- CSRF tokens: Random value verified on each request
- Origin validation: Check Origin/Referer headers
- Authorization headers: Use Bearer tokens instead of cookies
// CSRF Attack Example and Defense
// ❌ How CSRF works:
// 1. User logs into bank.com (session cookie stored)
// 2. User visits evil.com
// 3. Evil.com has hidden form:
/*
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="to" value="attacker">
</form>
<script>document.forms[0].submit()</script>
*/
// 4. Browser sends cookies automatically
// 5. Bank thinks it's legitimate request
// ✅ CSRF Token Protection
class CSRFProtection {
constructor() {
this.token = this.generateToken();
}
generateToken() {
// Generate cryptographically random token
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}
// Add token to all requests
addToRequest(options = {}) {
return {
...options,
headers: {
...options.headers,
'X-CSRF-Token': this.token
}
};
}
// Validate token on "server" side
validateToken(requestToken) {
return requestToken === this.token;
}
}
const csrf = new CSRFProtection();
console.log("CSRF Token:", csrf.token.substring(0, 20) + "...");
// Making a protected request
const fetchOptions = csrf.addToRequest({
method: 'POST',
body: JSON.stringify({ amount: 100 })
});
console.log("\nProtected request headers:");
console.log(fetchOptions.headers);
// Validation
console.log("\nValid token?", csrf.validateToken(csrf.token));
console.log("Invalid token?", csrf.validateToken("fake-token"));Input Sanitization — Clean Everything
Any time user input appears in HTML, URLs, DOM, or attributes — it must be sanitized.
// Comprehensive Input Sanitization
// Basic sanitizer for plain text
function sanitizeText(input) {
if (typeof input !== 'string') return '';
return input
// Remove null bytes
.replace(/\0/g, '')
// Normalize unicode
.normalize('NFC')
// Trim whitespace
.trim()
// Limit length
.slice(0, 1000);
}
// HTML sanitizer (basic - use DOMPurify in production)
function sanitizeHTML(html) {
// Create temporary element
const temp = document.createElement('div');
temp.textContent = html;
return temp.innerHTML;
}
// URL sanitizer
function sanitizeURL(url) {
try {
const parsed = new URL(url, window.location.origin);
// Only allow http/https protocols
if (!['http:', 'https:'].includes(parsed.protocol)) {
return 'about:blank';
}
// Optionally: only allow same origin
// if (parsed.origin !== window.location.origin) {
// return 'about:blank';
// }
return parsed.href;
} catch {
return 'about:blank';
}
}
// Test cases
console.log("=== Text Sanitization ===");
console.log(sanitizeText(" Hello World "));
console.log(sanitizeText("<script>alert(1)</script>"));
console.log("\n=== HTML Sanitization ===");
console.log(sanitizeHTML("<b>Bold</b>"));
console.log(sanitizeHTML('<img src=x onerror="alert(1)">'));
console.log("\n=== URL Sanitization ===");
console.log(sanitizeURL("https://example.com"));
console.log(sanitizeURL("javascript:alert(1)"));
console.log(sanitizeURL("data:text/html,<script>"));Use DOMPurify for production HTML sanitization. It handles SVG attacks, mutation XSS, and edge cases that simple regex cannot.
Advanced XSS Vectors
Even when developers escape <script> tags, attackers exploit less obvious injection points.
// Advanced XSS Vectors to Watch For
// 1. Event Handler Injection
const userAvatar = 'x" onerror="stealCookies()';
// ❌ DANGEROUS:
// `<img src="${userAvatar}">`
// Results in: <img src="x" onerror="stealCookies()">
// ✅ SAFE: Escape attribute values
function escapeAttr(str) {
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
console.log("Safe attribute:", escapeAttr(userAvatar));
// 2. Template Literal Injection
const userName = '${alert(1)}';
// In some template engines, this could execute!
// Always sanitize before template insertion
// 3. SVG-based XSS
const maliciousSVG = '<svg onload="alert(1)"></svg>';
console.log("\nSVG attack (escaped):", escapeAttr(maliciousSVG));
// 4. CSS Injection (in older browsers)
const cssAttack = 'expression(alert(1))';
// Can execute JS via CSS in old IE
// 5. Unicode Bypass Attempts
const unicodeAttack = '\u003cscript\u003e'; // <script> in unicode
console.log("Unicode decoded:", JSON.parse('"' + unicodeAttack + '"'));
// 6. Double Encoding Bypass
const doubleEncoded = '%253Cscript%253E'; // %3C = <
console.log("Double encoded attack:", decodeURIComponent(decodeURIComponent(doubleEncoded)));
// Defense: Always sanitize at the LAST moment before rendering
// Never trust data that has been "already sanitized" from elsewhereSecure Token Handling
Token theft is the ultimate goal of most XSS attacks. Never store sensitive tokens in localStorage — XSS can steal them.
❌ Dangerous Token Storage
- localStorage (XSS can read it)
- sessionStorage (XSS can read it)
- URL parameters (visible in logs)
- Non-HttpOnly cookies (JS can access)
// Secure Token Handling
// ❌ DANGEROUS - Token in localStorage (XSS can steal it)
// localStorage.setItem('token', 'secret-jwt-token');
// ❌ DANGEROUS - Token in URL
// window.location = '/dashboard?token=secret';
// ✅ BETTER - Token in memory (cleared on page refresh)
class SecureTokenStore {
#accessToken = null;
#tokenExpiry = null;
setToken(token, expiresIn = 3600) {
this.#accessToken = token;
this.#tokenExpiry = Date.now() + (expiresIn * 1000);
}
getToken() {
if (!this.#accessToken) return null;
// Check expiry
if (Date.now() > this.#tokenExpiry) {
this.clearToken();
return null;
}
return this.#accessToken;
}
clearToken() {
this.#accessToken = null;
this.#tokenExpiry = null;
}
isValid() {
return this.getToken() !== null;
}
}
const tokenStore = new SecureTokenStore();
tokenStore.setToken('my-access-token', 3600);
console.log("Token valid?", tokenStore.isValid());
console.log("Token value:", tokenStore.getToken() ? "***hidden***" : "none");
// For API calls, use Authorization header
function secureApiCall(url, options = {}) {
const token = tokenStore.getToken();
if (!token) {
throw new Error('No valid token');
}
return {
url,
options: {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
}
};
}
console.log("\nSecure API call config:");
console.log(secureApiCall('/api/data'));Content Security Policy (CSP)
CSP is a browser-level defense that restricts what scripts can run. Even if an attacker injects a script, CSP can block it from executing.
// Content Security Policy (CSP) - Browser-Level XSS Protection
// CSP is set via HTTP header by the server:
// Content-Security-Policy: default-src 'self'; script-src 'self'
// Example CSP configurations:
const cspExamples = {
// Strict - Only same-origin resources
strict: {
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self'",
"img-src": "'self' data:",
"connect-src": "'self'",
"frame-ancestors": "'none'",
"object-src": "'none'"
},
// Moderate - Allow specific CDNs
moderate: {
"default-src": "'self'",
"script-src": "'self' https://cdn.jsdelivr.net",
"style-src": "'self' 'unsafe-inline'", // Allow inline styles
"img-src": "'self' https: data:",
"connect-src": "'self' https://api.example.com"
},
// With nonce for inline scripts (most secure for SPAs)
withNonce: {
"script-src": "'self' 'nonce-abc123'",
// Only scripts with matching nonce will run:
// <script nonce="abc123">safe code</script>
}
};
// Convert to header string
function toCSPHeader(policy) {
return Object.entries(policy)
.map(([key, value]) => `${key} ${value}`)
.join('; ');
}
console.log("Strict CSP:");
console.log(toCSPHeader(cspExamples.strict));
console.log("\nModerate CSP:");
console.log(toCSPHeader(cspExamples.moderate));
// What CSP blocks:
console.log("\n❌ CSP blocks:");
console.log("- Inline scripts without nonce");
console.log("- Scripts from non-whitelisted domains");
console.log("- eval() and new Function()");
console.log("- Inline event handlers (onclick, etc.)");
console.log("- javascript: URLs");Defense-in-Depth Architecture
No single mechanism protects a modern app. You need layered security — if one layer fails, others prevent disaster.
// Defense-in-Depth Security Architecture
class SecureApp {
constructor() {
this.csrfToken = this.generateCSRFToken();
}
// Layer 1: Input Validation
validateInput(input, rules) {
const errors = [];
if (rules.required && !input) {
errors.push('Field is required');
}
if (rules.maxLength && input.length > rules.maxLength) {
errors.push(`Max length is ${rules.maxLength}`);
}
if (rules.pattern && !rules.pattern.test(input)) {
errors.push('Invalid format');
}
return { valid: errors.length === 0, errors };
}
// Layer 2: Sanitization
sanitize(input, type = 'text') {
if (type === 'text') {
return String(input).replace(/[<>]/g, '');
}
if (type === 'html') {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
if (type === 'url') {
try {
const url = new URL(input);
if (!['http:', 'https:'].includes(url.protocol)) {
return 'about:blank';
}
return url.href;
} catch {
return 'about:blank';
}
}
return '';
}
// Layer 3: CSRF Protection
generateCSRFToken() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}
// Layer 4: Safe Rendering
safeRender(text, element) {
// Always use textContent for user data
const span = document.createElement('span');
span.textContent = text;
return span.outerHTML;
}
// Layer 5: Secure API Calls
async secureRequest(url, options = {}) {
return {
url,
...options,
headers: {
...options.headers,
'X-CSRF-Token': this.csrfToken,
'Content-Type': 'application/json'
},
credentials: 'same-origin' // Don't send cookies cross-origin
};
}
}
// Usage
const app = new SecureApp();
// Validate
const validation = app.validateInput('[email protected]', {
required: true,
maxLength: 100,
pattern: /^[^@]+@[^@]+\.[^@]+$/
});
console.log("Validation:", validation);
// Sanitize
console.log("\nSanitized text:", app.sanitize('<script>alert(1)</script>'));
console.log("Sanitized URL:", app.sanitize('javascript:alert(1)', 'url'));
// Safe render
console.log("\nSafe HTML:", app.safeRender('<b>Bold</b>'));
console.log("\nCSRF Token:", app.csrfToken);Security Best Practices Checklist
// Security Best Practices Checklist
const securityChecklist = {
// XSS Prevention
xss: {
"Use textContent instead of innerHTML": true,
"Escape HTML entities": true,
"Use DOMPurify for HTML sanitization": true,
"Validate URL protocols": true,
"Never use eval()": true,
"Implement CSP headers": true
},
// CSRF Prevention
csrf: {
"Use CSRF tokens": true,
"SameSite cookie attribute": true,
"Verify Origin header": true,
"Use POST for state changes": true
},
// Token Security
tokens: {
"HttpOnly cookies for sessions": true,
"Secure flag on cookies": true,
"Short token expiry": true,
"Don't store tokens in localStorage": true,
"Use Authorization header": true
},
// General
general: {
"Input validation": true,
"Output encoding": true,
"HTTPS everywhere": true,
"Audit dependencies": true,
"Minimize third-party scripts": true
}
};
// Display checklist
function displayChecklist(checklist) {
for (const [category, items] of Object.entries(checklist)) {
console.log(`\n=== ${category.toUpperCase()} ===`);
for (const [item, checked] of Object.entries(items)) {
console.log(`${checked ? '✅' : '❌'} ${item}`);
}
}
}
displayChecklist(securityChecklist);
// Quick security test
function quickSecurityAudit(code) {
const risks = [];
if (code.includes('innerHTML')) {
risks.push('⚠️ innerHTML usage detected - ensure sanitization');
}
if (code.includes('eval(')) {
risks.push('🚨 eval() usage - CRITICAL RISK');
}
if (code.includes('document.write')) {
risks.push('⚠️ document.write usage - XSS risk');
}
if (code.includes('localStorage') && code.includes('token')) {
risks.push('⚠️ Token in localStorage - XSS can steal it');
}
return risks.length > 0 ? risks : ['✅ No obvious risks detected'];
}
console.log("\n=== Code Audit Example ===");
const testCode = 'element.innerHTML = userInput; localStorage.setItem("token", jwt);';
console.log("Risks:", quickSecurityAudit(testCode));// 🎯 YOUR TURN — replace each ___ using the hint beside it.
function escapeHtml(text) {
return String(text)
// 1) & goes FIRST, or you would re-escape the & in < below.
.replaceAll("&", "___") // 👉 replace ___ with &
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
const comment = '<img src=x onerror="steal()">';
console.log("Raw: ", comment);
console.log("Escaped:", escapeHtml(comment));
// 2) An allow-list, not a block-list: anything not named is refused.
const allowed = new ___(["/home", "/about"]); // 👉 replace ___ with Set
function safeRedirect(target) {
// 3) Ask the allow-list whether it holds this exact value.
return allowed.___(target) ? target : "/home"; // 👉 replace ___ with has
}
console.log("Good:", safeRedirect("/about"));
console.log("Bad: ", safeRedirect("https://evil.example/steal"));
// 4) Parse a query string properly instead of splitting on & and = yourself.
const params = new ___("q=hello&page=2"); // 👉 replace ___ with URLSearchParams
console.log("Query:", params.get("q"), "page", params.get("page"));
// 5) Send only the fields the client needs. Never the whole record.
const user = { name: "ada", role: "admin" };
const safeCopy = { name: user.___ }; // 👉 replace ___ with name
console.log("Sent to client:", JSON.stringify(safeCopy));
console.log("Never do this:", typeof eval === "function" ? "eval exists, do not feed it input" : "no eval");
// ✅ Expected output:
// Raw: <img src=x onerror="steal()">
// Escaped: <img src=x onerror="steal()">
// Good: /about
// Bad: /home
// Query: hello page 2
// Sent to client: {"name":"ada"}
// Never do this: eval exists, do not feed it input🛡️ Security Mastery Summary
- XSS Prevention: Use textContent, escape HTML, sanitize with DOMPurify
- CSRF Protection: Use tokens, SameSite cookies, validate Origin headers
- Input Sanitization: Validate, sanitize, and escape ALL user input
- Token Security: Use HttpOnly cookies, memory storage, short expiry
- CSP: Implement strict Content Security Policy headers
- URL Safety: Validate protocols, prevent open redirects
- Defense-in-Depth: Layer multiple security mechanisms
- Never trust: localStorage, URL params, API responses, cookies
Practice quiz
What does XSS stand for?
- Extended Style Sheets
- XML Server Security
- Cross-Site Scripting
- Cross-Server Sync
Answer: Cross-Site Scripting. XSS is Cross-Site Scripting, where malicious script executes inside your page.
Which is the safe way to display untrusted user input?
- element.textContent = input
- element.innerHTML = input
- eval(input)
- document.write(input)
Answer: element.textContent = input. textContent renders input as plain text, not executable HTML.
What does HTML escaping do?
- Removes all text
- Encrypts the page
- Deletes scripts from disk
- Converts dangerous characters like < and > into safe HTML entities
Answer: Converts dangerous characters like < and > into safe HTML entities. Escaping turns characters like < > & " into entities so they can't form active markup.
What is CSRF?
- A faster fetch API
- Forcing a victim's browser to make authenticated requests without their knowledge
- A CSS framework
- A caching strategy
Answer: Forcing a victim's browser to make authenticated requests without their knowledge. Cross-Site Request Forgery tricks the browser into sending authenticated requests.
Which is a recommended CSRF defense in the lesson?
- CSRF tokens and SameSite cookies
- Store tokens in the URL
- Use eval for validation
- Disable HTTPS
Answer: CSRF tokens and SameSite cookies. CSRF tokens, SameSite cookies, and Origin validation defend against CSRF.
Why should you NOT store sensitive tokens in localStorage?
- It is too slow
- It has no space
- XSS can read it
- It is deprecated
Answer: XSS can read it. localStorage is readable by JavaScript, so an XSS attack can steal the token.
When validating a URL from user input, which protocols should be allowed?
- javascript: and data:
- http: and https: only
- any protocol
- file: only
Answer: http: and https: only. Only allow http/https; reject javascript: and data: which can execute code.
What is Content Security Policy (CSP)?
- A linter rule
- A password manager
- A bundler plugin
- A browser-level defense restricting which scripts can run
Answer: A browser-level defense restricting which scripts can run. CSP is set via HTTP header and restricts script sources, blocking injected scripts.
Which dangerous function should you never call with user input?
- JSON.parse()
- eval()
- Array.map()
- String.trim()
Answer: eval(). eval() executes arbitrary code and must never run untrusted input.
What does defense-in-depth mean for a secure app?
- One strong firewall is enough
- Only validate on the server
- Layer multiple security mechanisms so others hold if one fails
- Encrypt the CSS
Answer: Layer multiple security mechanisms so others hold if one fails. Layered security (validation, sanitization, CSP, tokens) means one failure doesn't doom the app.
Continue this course
- Previous: Deep Dive into JSON, Parsing & Serialization
- Next: Working with WebSockets for Real-Time Features — Build live chat, notifications, and dashboards with WebSocket connections
- Quick reference: JavaScript cheat sheet