Cross-Site Scripting (XSS)
XSS occurs when attackers inject malicious scripts into web pages viewed by other users.
// VULNERABLE: Using innerHTML with user input
const userInput = "<script>alert('XSS')</script>";
element.innerHTML = userInput; // Executes the script!
// VULNERABLE: Using document.write
document.write(userInput); // Dangerous!
// SAFE: Use textContent instead
element.textContent = userInput; // Displays as text
// SAFE: Escape HTML entities
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Or manual escaping
function escapeHTMLManual(str) {
return str
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// SAFE: Create elements programmatically
const link = document.createElement("a");
link.href = sanitizeURL(userProvidedURL);
link.textContent = userProvidedText;
container.appendChild(link);
// Sanitize URLs
function sanitizeURL(url) {
try {
const parsed = new URL(url);
// Only allow http and https
if (!["http:", "https:"].includes(parsed.protocol)) {
return "";
}
return url;
} catch {
return "";
}
}
// DANGEROUS: javascript: URLs
// <a href="javascript:alert('XSS')">Click</a>
// Always validate URL protocols!
Any data from users (form inputs, URL parameters, cookies) should be treated as potentially malicious and properly sanitized before use.
Cross-Site Request Forgery (CSRF)
CSRF tricks users into performing unwanted actions on sites where they're authenticated.
// CSRF Protection: Include token in requests
// 1. Get CSRF token from meta tag or cookie
function getCSRFToken() {
// From meta tag
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta) return meta.content;
// Or from cookie
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : null;
}
// 2. Include token in fetch requests
async function secureRequest(url, options = {}) {
const token = getCSRFToken();
return fetch(url, {
...options,
headers: {
...options.headers,
"X-CSRF-Token": token,
"Content-Type": "application/json"
},
credentials: "same-origin" // Include cookies
});
}
// 3. Verify token server-side
// Server checks X-CSRF-Token header matches session token
// SameSite Cookie attribute (set by server)
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// SameSite options:
// Strict - Cookie never sent cross-site
// Lax - Sent with top-level navigations
// None - Sent always (requires Secure)
// Avoid state-changing GET requests
// BAD: GET /api/delete-account
// GOOD: POST /api/delete-account with CSRF token
Injection Attacks
Prevent code injection through proper input handling.
// NEVER use eval with user input
// DANGEROUS:
eval(userInput); // Can execute arbitrary code!
// DANGEROUS: new Function with user input
new Function(userInput)();
// DANGEROUS: setTimeout/setInterval with strings
setTimeout(userInput, 1000); // Can execute code!
// SAFE: Use function reference
setTimeout(() => safeFunction(), 1000);
// JSON Parsing - use JSON.parse, not eval
// DANGEROUS:
const data = eval("(" + jsonString + ")");
// SAFE:
try {
const data = JSON.parse(jsonString);
} catch (e) {
console.error("Invalid JSON");
}
// Template literals - be careful with user input
// DANGEROUS if tag allows code execution:
html`${userInput}`; // Depends on html implementation
// SAFE: Escape first
html`${escapeHTML(userInput)}`;
// RegExp - escape user input
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const userSearch = "hello (world)";
const regex = new RegExp(escapeRegExp(userSearch), "i");
// SQL Injection (for Node.js with databases)
// NEVER concatenate user input into queries
// BAD:
`SELECT * FROM users WHERE id = ${userId}`
// GOOD: Use parameterized queries
db.query("SELECT * FROM users WHERE id = ?", [userId]);
Data Security
Protect sensitive data in transit and storage.
// ===== Sensitive Data Storage =====
// NEVER store sensitive data in localStorage
// localStorage is accessible to any script on the page
localStorage.setItem("password", "secret"); // BAD!
// Use httpOnly cookies for auth tokens (set by server)
// These can't be accessed by JavaScript
// For client-side encryption, use Web Crypto API
async function generateKey() {
return crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
async function encrypt(key, data) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(data);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
encoded
);
return { iv, ciphertext };
}
// ===== Secure Random Values =====
// NEVER use Math.random() for security
const insecure = Math.random(); // Predictable!
// SAFE: Use crypto API
const secureBytes = crypto.getRandomValues(new Uint8Array(32));
// Generate secure token
function generateToken(length = 32) {
const bytes = crypto.getRandomValues(new Uint8Array(length));
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// ===== Password Handling =====
// Never store passwords in plain text
// Always hash on the server with bcrypt/argon2
// Client-side: Clear sensitive data after use
function handleLogin(password) {
sendToServer(password);
password = null; // Clear reference
}
// ===== HTTPS =====
// Always use HTTPS in production
if (location.protocol !== "https:" && location.hostname !== "localhost") {
location.replace("https://" + location.host + location.pathname);
}
Content Security Policy
CSP helps prevent XSS by controlling which resources can be loaded.
<!-- CSP via meta tag -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;">
<!-- Or via HTTP header (preferred) -->
<!-- Content-Security-Policy: default-src 'self' -->
// CSP Directives:
// default-src - Fallback for other directives
// script-src - Valid sources for JavaScript
// style-src - Valid sources for CSS
// img-src - Valid sources for images
// connect-src - Valid sources for fetch/XHR/WebSocket
// font-src - Valid sources for fonts
// frame-src - Valid sources for iframes
// CSP values:
// 'self' - Same origin only
// 'none' - Block all
// 'unsafe-inline' - Allow inline scripts/styles (avoid!)
// 'unsafe-eval' - Allow eval() (avoid!)
// https: - Any HTTPS URL
// data: - Data URIs
// 'nonce-abc123' - Scripts with matching nonce
// 'sha256-...' - Scripts matching hash
// Using nonces for inline scripts
// Server generates random nonce per request
// <script nonce="abc123">...</script>
// CSP: script-src 'nonce-abc123'
// Report CSP violations
// Content-Security-Policy-Report-Only: ...
// report-uri /csp-violation-report
Security Best Practices
General guidelines for writing secure JavaScript code.
// 1. Use strict mode
"use strict";
// 2. Validate all inputs
function processUserData(data) {
if (!data || typeof data !== "object") {
throw new Error("Invalid data");
}
if (!isValidEmail(data.email)) {
throw new Error("Invalid email");
}
// Process validated data
}
// 3. Use Object.freeze for constants
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
TIMEOUT: 5000
});
// 4. Avoid global variables
(function() {
const privateVar = "secret";
// ...
})();
// 5. Use subresource integrity for CDN scripts
// <script src="https://cdn.example.com/lib.js"
// integrity="sha384-abc123..."
// crossorigin="anonymous"></script>
// 6. Set secure cookie options (server-side)
// Set-Cookie: session=token; Secure; HttpOnly; SameSite=Strict
// 7. Implement rate limiting
// Prevent brute force attacks on login
// 8. Log security events
function logSecurityEvent(type, details) {
console.warn(`[Security] ${type}:`, details);
// Send to monitoring service
}
// 9. Keep dependencies updated
// npm audit
// npm update
// 10. Use security headers (server-side)
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 1; mode=block
// Strict-Transport-Security: max-age=31536000
Authentication Best Practices
Secure authentication protects user accounts from unauthorized access. Follow these patterns to implement robust authentication.
// JWT handling
class AuthManager {
#accessToken = null;
#refreshToken = null;
setTokens(access, refresh) {
this.#accessToken = access;
this.#refreshToken = refresh;
// Store refresh token securely (HttpOnly cookie preferred)
// Never store in localStorage for sensitive apps
}
async getValidToken() {
if (!this.#accessToken) return null;
// Check if token is expired
const payload = this.decodeToken(this.#accessToken);
if (payload.exp * 1000 < Date.now()) {
return await this.refreshAccessToken();
}
return this.#accessToken;
}
decodeToken(token) {
// Decode payload (don't trust without server verification)
const payload = token.split('.')[1];
return JSON.parse(atob(payload));
}
async refreshAccessToken() {
try {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // Include HttpOnly cookie
});
if (!response.ok) {
this.logout();
return null;
}
const { accessToken } = await response.json();
this.#accessToken = accessToken;
return accessToken;
} catch {
this.logout();
return null;
}
}
logout() {
this.#accessToken = null;
this.#refreshToken = null;
// Clear cookies server-side
fetch('/api/logout', { method: 'POST', credentials: 'include' });
}
}
// Password strength validation
function validatePassword(password) {
const checks = {
length: password.length >= 12,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
};
const passed = Object.values(checks).filter(Boolean).length;
return {
valid: passed >= 4 && checks.length,
score: passed,
checks
};
}
// Brute force protection (client-side delay)
class LoginThrottle {
constructor() {
this.attempts = 0;
this.lockoutUntil = 0;
}
canAttempt() {
if (Date.now() < this.lockoutUntil) {
const remaining = Math.ceil((this.lockoutUntil - Date.now()) / 1000);
return { allowed: false, waitSeconds: remaining };
}
return { allowed: true };
}
recordFailure() {
this.attempts++;
if (this.attempts >= 5) {
// Exponential backoff: 30s, 60s, 120s...
const lockoutSeconds = Math.min(30 * Math.pow(2, this.attempts - 5), 3600);
this.lockoutUntil = Date.now() + lockoutSeconds * 1000;
}
}
recordSuccess() {
this.attempts = 0;
this.lockoutUntil = 0;
}
}
Input Validation & Sanitization
Validate and sanitize all user input to prevent injection attacks and data corruption.
// Schema-based validation
class Validator {
static string(value, { minLength = 0, maxLength = Infinity, pattern } = {}) {
if (typeof value !== 'string') return { valid: false, error: 'Must be a string' };
if (value.length < minLength) return { valid: false, error: `Minimum ${minLength} characters` };
if (value.length > maxLength) return { valid: false, error: `Maximum ${maxLength} characters` };
if (pattern && !pattern.test(value)) return { valid: false, error: 'Invalid format' };
return { valid: true };
}
static email(value) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!pattern.test(value)) return { valid: false, error: 'Invalid email' };
return { valid: true };
}
static number(value, { min = -Infinity, max = Infinity } = {}) {
const num = Number(value);
if (isNaN(num)) return { valid: false, error: 'Must be a number' };
if (num < min) return { valid: false, error: `Minimum value is ${min}` };
if (num > max) return { valid: false, error: `Maximum value is ${max}` };
return { valid: true };
}
static validate(data, schema) {
const errors = {};
for (const [field, rules] of Object.entries(schema)) {
const value = data[field];
const result = rules(value);
if (!result.valid) {
errors[field] = result.error;
}
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
}
// Usage
const userSchema = {
username: v => Validator.string(v, { minLength: 3, maxLength: 20, pattern: /^[a-zA-Z0-9_]+$/ }),
email: v => Validator.email(v),
age: v => Validator.number(v, { min: 13, max: 120 })
};
const result = Validator.validate(formData, userSchema);
// HTML sanitization
function sanitizeHTML(str) {
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
// More robust sanitization with DOMPurify
// import DOMPurify from 'dompurify';
// const clean = DOMPurify.sanitize(dirty);
// URL validation
function isValidURL(string) {
try {
const url = new URL(string);
return ['http:', 'https:'].includes(url.protocol);
} catch {
return false;
}
}
// Safe redirect
function safeRedirect(url, allowedDomains = []) {
try {
const parsed = new URL(url, window.location.origin);
// Only allow same-origin or whitelisted domains
if (parsed.origin === window.location.origin) {
window.location.href = url;
return true;
}
if (allowedDomains.includes(parsed.hostname)) {
window.location.href = url;
return true;
}
console.warn('Blocked redirect to:', url);
return false;
} catch {
console.error('Invalid URL:', url);
return false;
}
}
Secure Communication
Ensure all data transmission is encrypted and protected against interception.
// Force HTTPS
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}
// Secure fetch wrapper
async function secureFetch(url, options = {}) {
// Enforce HTTPS
const parsedURL = new URL(url, window.location.origin);
if (parsedURL.protocol !== 'https:' && parsedURL.hostname !== 'localhost') {
throw new Error('HTTPS required');
}
// Add security headers
const secureOptions = {
...options,
credentials: 'same-origin', // Or 'include' for cross-origin with cookies
headers: {
...options.headers,
'Content-Type': 'application/json'
}
};
// Add CSRF token if available
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (csrfToken) {
secureOptions.headers['X-CSRF-Token'] = csrfToken;
}
return fetch(url, secureOptions);
}
// Subresource Integrity check
async function loadScriptWithSRI(url, expectedHash) {
const response = await fetch(url);
const content = await response.text();
// Calculate hash
const encoder = new TextEncoder();
const data = encoder.encode(content);
const hashBuffer = await crypto.subtle.digest('SHA-384', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashBase64 = btoa(String.fromCharCode(...hashArray));
if (`sha384-${hashBase64}` !== expectedHash) {
throw new Error('Script integrity check failed');
}
// Safe to execute
const script = document.createElement('script');
script.textContent = content;
document.head.appendChild(script);
}
// Encrypt sensitive data before storage
async function encryptForStorage(data, key) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
dataBuffer
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
};
}
- Never trust user input - always validate and sanitize
- Use HTTPS for all production traffic
- Implement CSP to prevent XSS attacks
- Use HttpOnly cookies for sensitive tokens
- Keep dependencies updated - run npm audit regularly
- Limit permissions - principle of least privilege
Summary
XSS Prevention
Escape output, use textContent
CSRF Protection
Tokens, SameSite cookies
Avoid Injection
No eval, parameterized queries
Data Security
HTTPS, crypto API, httpOnly cookies
CSP
Control resource loading
Input Validation
Never trust user input