Skip to content

JavaScript Security

Security is paramount in modern web development. This comprehensive guide covers essential security practices, common vulnerabilities, and defensive programming techniques for JavaScript applications.

XSS attacks occur when malicious scripts are injected into trusted websites.

// VULNERABLE: Direct DOM manipulation with user input
function displayUserName(userName) {
document.getElementById('greeting').innerHTML = `Hello, ${userName}!`;
// If userName contains <script>alert('XSS')</script>, it will execute!
}
// SECURE: Use textContent or proper escaping
function displayUserNameSecure(userName) {
document.getElementById('greeting').textContent = `Hello, ${userName}!`;
// Or use innerHTML with proper escaping
document.getElementById('greeting').innerHTML = `Hello, ${escapeHtml(userName)}!`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// SECURE: Use template engines with auto-escaping
const template = `<div>Hello, {{username}}</div>`; // Handlebars auto-escapes
// SECURE: Content Security Policy headers
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

CSRF attacks trick users into performing unwanted actions on websites where they’re authenticated.

// VULNERABLE: Simple form submission without CSRF protection
async function deleteAccount() {
await fetch('/api/account/delete', {
method: 'DELETE',
credentials: 'include' // Sends cookies
});
}
// SECURE: CSRF token implementation
class CSRFProtection {
constructor() {
this.token = this.getCSRFToken();
}
getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}
async secureRequest(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': this.token,
...options.headers
};
return fetch(url, {
...options,
headers,
credentials: 'include'
});
}
}
const csrf = new CSRFProtection();
// Usage
async function deleteAccountSecure() {
try {
const response = await csrf.secureRequest('/api/account/delete', {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Delete failed');
}
} catch (error) {
console.error('Account deletion failed:', error);
}
}
// Server-side CSRF validation (Express.js example)
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});

Even in JavaScript applications, SQL injection can occur on the backend.

// VULNERABLE: String concatenation
async function getUserByIdVulnerable(userId) {
const query = `SELECT * FROM users WHERE id = ${userId}`;
return database.query(query);
// If userId is "1 OR 1=1", returns all users!
}
// SECURE: Parameterized queries
async function getUserByIdSecure(userId) {
const query = 'SELECT * FROM users WHERE id = ?';
return database.query(query, [userId]);
}
// SECURE: ORM with built-in protection
async function getUserWithORM(userId) {
return User.findById(userId); // Sequelize, TypeORM, etc.
}
// Input validation and sanitization
const Joi = require('joi');
const userIdSchema = Joi.number().integer().positive().required();
async function validateAndGetUser(userId) {
const { error, value } = userIdSchema.validate(userId);
if (error) {
throw new Error('Invalid user ID');
}
return getUserByIdSecure(value);
}
class InputValidator {
static email(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static password(password) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasNonalphas = /\W/.test(password);
return password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonalphas;
}
static sanitizeString(input) {
return input
.replace(/[<>]/g, '') // Remove angle brackets
.trim()
.slice(0, 255); // Limit length
}
static isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
static isAlphanumeric(input) {
return /^[a-zA-Z0-9]+$/.test(input);
}
}
// Comprehensive form validation
class FormValidator {
constructor(form) {
this.form = form;
this.errors = {};
}
validateField(fieldName, value, rules) {
this.errors[fieldName] = [];
rules.forEach(rule => {
if (rule.type === 'required' && !value.trim()) {
this.errors[fieldName].push('This field is required');
}
if (rule.type === 'email' && value && !InputValidator.email(value)) {
this.errors[fieldName].push('Invalid email format');
}
if (rule.type === 'password' && value && !InputValidator.password(value)) {
this.errors[fieldName].push('Password must be at least 8 characters with uppercase, lowercase, number, and special character');
}
if (rule.type === 'minLength' && value.length < rule.value) {
this.errors[fieldName].push(`Minimum length is ${rule.value} characters`);
}
if (rule.type === 'maxLength' && value.length > rule.value) {
this.errors[fieldName].push(`Maximum length is ${rule.value} characters`);
}
});
// Remove empty error arrays
if (this.errors[fieldName].length === 0) {
delete this.errors[fieldName];
}
}
validate(formData, validationRules) {
this.errors = {};
Object.keys(validationRules).forEach(fieldName => {
const value = formData[fieldName] || '';
const rules = validationRules[fieldName];
this.validateField(fieldName, value, rules);
});
return {
isValid: Object.keys(this.errors).length === 0,
errors: this.errors
};
}
}
// Usage example
const validator = new FormValidator();
const formData = {
email: 'user@example.com',
password: 'weak',
confirmPassword: 'weak'
};
const rules = {
email: [
{ type: 'required' },
{ type: 'email' }
],
password: [
{ type: 'required' },
{ type: 'password' },
{ type: 'minLength', value: 8 }
]
};
const validation = validator.validate(formData, rules);
// JWT token management
class JWTManager {
constructor() {
this.tokenKey = 'auth_token';
this.refreshKey = 'refresh_token';
}
setTokens(accessToken, refreshToken) {
// Store in httpOnly cookies (preferred) or secure storage
this.setSecureCookie(this.tokenKey, accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
this.setSecureCookie(this.refreshKey, refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
}
getToken() {
return this.getCookie(this.tokenKey);
}
removeTokens() {
this.deleteCookie(this.tokenKey);
this.deleteCookie(this.refreshKey);
}
async refreshToken() {
try {
const refreshToken = this.getCookie(this.refreshKey);
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
credentials: 'include'
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const { accessToken, refreshToken: newRefreshToken } = await response.json();
this.setTokens(accessToken, newRefreshToken);
return accessToken;
} catch (error) {
this.removeTokens();
window.location.href = '/login';
throw error;
}
}
setSecureCookie(name, value, options) {
const optionsString = Object.entries(options)
.map(([key, val]) => `${key}=${val}`)
.join('; ');
document.cookie = `${name}=${value}; ${optionsString}`;
}
getCookie(name) {
const match = document.cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`);
return match ? match.pop() : null;
}
deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
}
// Secure API client with automatic token refresh
class SecureAPIClient {
constructor() {
this.jwtManager = new JWTManager();
this.isRefreshing = false;
this.refreshPromise = null;
}
async request(url, options = {}) {
const token = this.jwtManager.getToken();
const config = {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.headers
},
credentials: 'include'
};
try {
let response = await fetch(url, config);
// Handle token expiration
if (response.status === 401 && token) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshPromise = this.jwtManager.refreshToken();
}
await this.refreshPromise;
this.isRefreshing = false;
this.refreshPromise = null;
// Retry original request with new token
const newToken = this.jwtManager.getToken();
config.headers.Authorization = `Bearer ${newToken}`;
response = await fetch(url, config);
}
return response;
} catch (error) {
this.isRefreshing = false;
this.refreshPromise = null;
throw error;
}
}
async get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' });
}
async post(url, data, options = {}) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
}
class RBACManager {
constructor(userRoles, permissions) {
this.userRoles = userRoles;
this.permissions = permissions;
}
hasPermission(permission) {
return this.userRoles.some(role =>
this.permissions[role] && this.permissions[role].includes(permission)
);
}
hasRole(role) {
return this.userRoles.includes(role);
}
canAccess(resource, action = 'read') {
const permission = `${resource}:${action}`;
return this.hasPermission(permission);
}
}
// Permission definitions
const permissions = {
admin: ['users:read', 'users:write', 'users:delete', 'posts:read', 'posts:write', 'posts:delete'],
moderator: ['posts:read', 'posts:write', 'posts:delete', 'users:read'],
user: ['posts:read', 'profile:read', 'profile:write'],
guest: ['posts:read']
};
// Usage
const rbac = new RBACManager(['user'], permissions);
function SecureComponent({ children, requiredPermission }) {
if (!rbac.hasPermission(requiredPermission)) {
return <div>Access denied</div>;
}
return children;
}
// React component usage
<SecureComponent requiredPermission="users:write">
<UserEditForm />
</SecureComponent>
// Client-side encryption (for sensitive data)
class ClientEncryption {
async generateKey() {
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
);
}
async encrypt(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encodedData
);
return {
encrypted: Array.from(new Uint8Array(encrypted)),
iv: Array.from(iv)
};
}
async decrypt(encryptedData, key, iv) {
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(iv)
},
key,
new Uint8Array(encryptedData)
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
}
// Secure password hashing (server-side with Node.js)
const bcrypt = require('bcrypt');
class PasswordManager {
static async hashPassword(password) {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
static async verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
static generateSecurePassword(length = 16) {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
}
// Usage
async function registerUser(email, password) {
const hashedPassword = await PasswordManager.hashPassword(password);
// Store hashedPassword in database
return User.create({
email,
password: hashedPassword
});
}
async function loginUser(email, password) {
const user = await User.findOne({ email });
if (!user) {
throw new Error('User not found');
}
const isValid = await PasswordManager.verifyPassword(password, user.password);
if (!isValid) {
throw new Error('Invalid password');
}
return user;
}
// CSP header configuration
const cspDirectives = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-inline'", // Avoid if possible
'https://cdn.jsdelivr.net',
'https://unpkg.com'
],
'style-src': [
"'self'",
"'unsafe-inline'", // Often necessary for CSS
'https://fonts.googleapis.com'
],
'img-src': [
"'self'",
'data:',
'https:'
],
'font-src': [
"'self'",
'https://fonts.gstatic.com'
],
'connect-src': [
"'self'",
'https://api.example.com'
],
'frame-src': ["'none'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"]
};
function generateCSPHeader() {
return Object.entries(cspDirectives)
.map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
.join('; ');
}
// Express.js middleware
function cspMiddleware(req, res, next) {
const csp = generateCSPHeader();
res.setHeader('Content-Security-Policy', csp);
next();
}
// CSP violation reporting
function setupCSPReporting() {
window.addEventListener('securitypolicyviolation', (event) => {
const violation = {
blockedURI: event.blockedURI,
documentURI: event.documentURI,
effectiveDirective: event.effectiveDirective,
originalPolicy: event.originalPolicy,
referrer: event.referrer,
violatedDirective: event.violatedDirective,
timestamp: Date.now()
};
// Send violation report to server
fetch('/api/csp-violation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(violation)
});
});
}
// Force HTTPS redirect
function enforceHTTPS() {
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace('https:' + window.location.href.substring(window.location.protocol.length));
}
}
// Certificate pinning (for mobile apps)
class CertificatePinner {
constructor(pinnedCertificates) {
this.pinnedCerts = pinnedCertificates;
}
async verifyConnection(url) {
try {
const response = await fetch(url, {
method: 'HEAD'
});
// In a real implementation, you would verify the certificate
// This is a simplified example
const cert = this.getCertificateInfo(response);
if (!this.isPinnedCertificate(cert)) {
throw new Error('Certificate pinning validation failed');
}
return true;
} catch (error) {
console.error('Certificate pinning failed:', error);
return false;
}
}
getCertificateInfo(response) {
// Extract certificate information
// In practice, this would involve more complex certificate parsing
return response.headers.get('x-certificate-fingerprint');
}
isPinnedCertificate(cert) {
return this.pinnedCerts.includes(cert);
}
}
// HSTS (HTTP Strict Transport Security)
function setHSTSHeader(res) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
// Dependency vulnerability scanning
const auditScript = `
npm audit --audit-level=moderate
npm audit fix --force
`;
// Custom security audit function
class SecurityAuditor {
constructor() {
this.vulnerabilities = [];
}
checkForInsecurePatterns(code) {
const patterns = [
{
pattern: /eval\s*\(/g,
severity: 'high',
message: 'Use of eval() is dangerous and should be avoided'
},
{
pattern: /innerHTML\s*=/g,
severity: 'medium',
message: 'innerHTML can lead to XSS vulnerabilities'
},
{
pattern: /document\.write\s*\(/g,
severity: 'medium',
message: 'document.write() can be dangerous'
},
{
pattern: /\$\{.*\}/g,
severity: 'low',
message: 'Template literals with user input should be carefully reviewed'
}
];
patterns.forEach(({ pattern, severity, message }) => {
const matches = code.match(pattern);
if (matches) {
this.vulnerabilities.push({
severity,
message,
occurrences: matches.length,
pattern: pattern.source
});
}
});
return this.vulnerabilities;
}
generateReport() {
const severityCounts = this.vulnerabilities.reduce((acc, vuln) => {
acc[vuln.severity] = (acc[vuln.severity] || 0) + 1;
return acc;
}, {});
return {
summary: severityCounts,
details: this.vulnerabilities,
riskScore: this.calculateRiskScore()
};
}
calculateRiskScore() {
const weights = { high: 10, medium: 5, low: 1 };
return this.vulnerabilities.reduce((score, vuln) => {
return score + weights[vuln.severity];
}, 0);
}
}
// Usage
const auditor = new SecurityAuditor();
const codeToAudit = `
function displayUserInput(input) {
document.getElementById('content').innerHTML = input;
eval('console.log("User input: " + input)');
}
`;
const vulnerabilities = auditor.checkForInsecurePatterns(codeToAudit);
const report = auditor.generateReport();
console.log('Security Report:', report);

This comprehensive security guide provides essential practices for building secure JavaScript applications. Remember that security is an ongoing process that requires constant vigilance and updates.


Always stay updated with the latest security advisories and regularly audit your applications for vulnerabilities.