XSS Prevention and Content Security Policy in JavaScript
Cross-Site Scripting (XSS) is a critical vulnerability. This article covers XSS types, prevention techniques, and Content Security Policy implementation.
Introduction
XSS prevention provides:
- Attack protection
- Data security
- User safety
- Application integrity
- Compliance
Understanding XSS helps you:
- Identify vulnerabilities
- Implement defenses
- Use CSP effectively
- Secure applications
- Protect users
XSS Attack Types
Stored XSS
// โ Bad: Vulnerable to stored XSS
// User input stored in database
const userComment = '<img src=x onerror="alert(\'XSS\')">';
database.save('comments', userComment);
// Later, when displayed:
document.getElementById('comments').innerHTML = userComment; // Executes!
// โ
Good: Sanitize before storing
const sanitized = escapeHTML(userComment);
database.save('comments', sanitized);
// Display safely:
document.getElementById('comments').textContent = sanitized;
Reflected XSS
// โ Bad: Vulnerable to reflected XSS
// URL: http://example.com?search=<script>alert('XSS')</script>
const searchParam = new URLSearchParams(window.location.search).get('search');
document.getElementById('results').innerHTML = `Search results for: ${searchParam}`;
// โ
Good: Sanitize URL parameters
const searchParam = new URLSearchParams(window.location.search).get('search');
const sanitized = escapeHTML(searchParam);
document.getElementById('results').textContent = `Search results for: ${sanitized}`;
DOM-based XSS
// โ Bad: Vulnerable to DOM-based XSS
function updateProfile(data) {
document.getElementById('profile').innerHTML = data.bio; // Dangerous!
}
// โ
Good: Use textContent or sanitize
function updateProfile(data) {
document.getElementById('profile').textContent = data.bio; // Safe
}
// Or sanitize if HTML is needed:
function updateProfileHTML(data) {
const sanitized = DOMPurify.sanitize(data.bio);
document.getElementById('profile').innerHTML = sanitized;
}
XSS Prevention Techniques
Output Encoding
// โ
Good: Encode output based on context
function encodeHTML(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, (char) => map[char]);
}
function encodeURL(url) {
return encodeURIComponent(url);
}
function encodeJavaScript(str) {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}
// Usage
const userInput = '<script>alert("XSS")</script>';
console.log(encodeHTML(userInput));
// <script>alert("XSS")</script>
Input Validation
// โ
Good: Validate input
function validateUserInput(input, type = 'text') {
switch (type) {
case 'email':
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input);
case 'url':
try {
new URL(input);
return true;
} catch {
return false;
}
case 'text':
return typeof input === 'string' && input.length > 0;
case 'number':
return !isNaN(input) && isFinite(input);
default:
return false;
}
}
// Usage
console.log(validateUserInput('[email protected]', 'email')); // true
console.log(validateUserInput('<script>', 'text')); // true (but will be encoded)
Content Security Policy (CSP)
// โ
Good: Implement CSP headers (server-side)
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' https://fonts.googleapis.com
// Directives:
// default-src: Default policy for all content
// script-src: Allowed sources for scripts
// style-src: Allowed sources for stylesheets
// img-src: Allowed sources for images
// font-src: Allowed sources for fonts
// connect-src: Allowed sources for fetch/XHR
// frame-src: Allowed sources for iframes
// object-src: Allowed sources for plugins
// Client-side: Check CSP violations
document.addEventListener('securitypolicyviolation', (e) => {
console.error('CSP violation:', {
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy
});
});
Strict CSP
// โ
Good: Strict CSP with nonce
// Server generates nonce for each request
const nonce = generateRandomNonce();
// Header:
// Content-Security-Policy: script-src 'nonce-${nonce}'; default-src 'self'
// HTML:
// <script nonce="${nonce}">
// // Inline script allowed only with matching nonce
// </script>
// JavaScript:
function generateRandomNonce() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
function addScriptWithNonce(code, nonce) {
const script = document.createElement('script');
script.nonce = nonce;
script.textContent = code;
document.head.appendChild(script);
}
Secure DOM Manipulation
Safe Element Creation
// โ
Good: Safe element creation
function createSafeElement(tag, content, attributes = {}) {
const element = document.createElement(tag);
element.textContent = content; // Safe - no HTML parsing
for (const [key, value] of Object.entries(attributes)) {
// Validate attribute
if (isAllowedAttribute(key)) {
element.setAttribute(key, value);
}
}
return element;
}
function isAllowedAttribute(attr) {
const allowedAttrs = ['class', 'id', 'data-*', 'aria-*'];
return allowedAttrs.some(allowed => {
if (allowed.endsWith('-*')) {
return attr.startsWith(allowed.slice(0, -2));
}
return attr === allowed;
});
}
// Usage
const div = createSafeElement('div', '<script>alert("XSS")</script>', {
class: 'container'
});
// Creates: <div class="container"><script>alert("XSS")</script></div>
Template Literals Safely
// โ
Good: Use template literals with encoding
function renderUserProfile(user) {
const encodedName = encodeHTML(user.name);
const encodedBio = encodeHTML(user.bio);
return `
<div class="profile">
<h1>${encodedName}</h1>
<p>${encodedBio}</p>
</div>
`;
}
// โ Bad: Direct interpolation
function renderUserProfileUnsafe(user) {
return `
<div class="profile">
<h1>${user.name}</h1>
<p>${user.bio}</p>
</div>
`;
}
// Usage
const user = { name: '<img src=x onerror="alert(\'XSS\')">', bio: 'Developer' };
const html = renderUserProfile(user);
document.getElementById('profile').innerHTML = html; // Safe
Using Security Libraries
DOMPurify
// โ
Good: Use DOMPurify for HTML sanitization
// Include: <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
function sanitizeHTML(html) {
return DOMPurify.sanitize(html);
}
function sanitizeWithConfig(html, config = {}) {
const defaultConfig = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href', 'title'],
KEEP_CONTENT: true
};
return DOMPurify.sanitize(html, { ...defaultConfig, ...config });
}
// Usage
const userHTML = '<p>Hello <script>alert("XSS")</script></p>';
const safe = sanitizeHTML(userHTML);
// <p>Hello </p>
document.getElementById('content').innerHTML = safe;
Trusted Types API
// โ
Good: Use Trusted Types API (modern browsers)
if (window.trustedTypes) {
const policy = trustedTypes.createPolicy('default', {
createHTML: (string) => {
// Sanitize HTML
return DOMPurify.sanitize(string);
},
createScript: (string) => {
// Validate script
if (isAllowedScript(string)) {
return string;
}
throw new Error('Script not allowed');
},
createScriptURL: (string) => {
// Validate script URL
if (isAllowedURL(string)) {
return string;
}
throw new Error('URL not allowed');
}
});
// Now innerHTML only accepts TrustedHTML
document.getElementById('content').innerHTML = policy.createHTML(userHTML);
}
function isAllowedScript(script) {
// Implement validation logic
return true;
}
function isAllowedURL(url) {
// Implement validation logic
return url.startsWith('https://');
}
Practical Security Examples
Secure Comment System
// โ
Good: Secure comment system
class SecureCommentSystem {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.comments = [];
}
addComment(author, text) {
// Validate input
if (!author || !text) {
throw new Error('Author and text are required');
}
// Sanitize input
const sanitizedAuthor = escapeHTML(author.trim());
const sanitizedText = escapeHTML(text.trim());
// Store comment
const comment = {
id: Date.now(),
author: sanitizedAuthor,
text: sanitizedText,
timestamp: new Date()
};
this.comments.push(comment);
this.render();
}
render() {
this.container.innerHTML = '';
this.comments.forEach(comment => {
const div = document.createElement('div');
div.className = 'comment';
const author = document.createElement('strong');
author.textContent = comment.author;
const text = document.createElement('p');
text.textContent = comment.text;
const time = document.createElement('small');
time.textContent = comment.timestamp.toLocaleString();
div.appendChild(author);
div.appendChild(text);
div.appendChild(time);
this.container.appendChild(div);
});
}
deleteComment(id) {
this.comments = this.comments.filter(c => c.id !== id);
this.render();
}
}
// Usage
const comments = new SecureCommentSystem('comments-container');
comments.addComment('John', 'Great article!');
comments.addComment('Jane', '<script>alert("XSS")</script>'); // Safely escaped
Secure User Profile Display
// โ
Good: Secure user profile
class UserProfile {
constructor(user) {
this.user = user;
}
render() {
const container = document.createElement('div');
container.className = 'user-profile';
// Name
const name = document.createElement('h1');
name.textContent = this.user.name;
// Email
const email = document.createElement('p');
email.textContent = `Email: ${this.user.email}`;
// Bio
const bio = document.createElement('p');
bio.textContent = this.user.bio;
// Avatar (with URL validation)
const avatar = document.createElement('img');
const avatarURL = this.validateImageURL(this.user.avatarURL);
if (avatarURL) {
avatar.src = avatarURL;
avatar.alt = this.user.name;
}
container.appendChild(avatar);
container.appendChild(name);
container.appendChild(email);
container.appendChild(bio);
return container;
}
validateImageURL(url) {
try {
const parsed = new URL(url);
// Only allow https
if (parsed.protocol !== 'https:') {
return null;
}
// Only allow specific domains
const allowedDomains = ['cdn.example.com', 'images.example.com'];
if (!allowedDomains.includes(parsed.hostname)) {
return null;
}
return url;
} catch {
return null;
}
}
}
// Usage
const user = {
name: 'John Doe',
email: '[email protected]',
bio: 'Software developer',
avatarURL: 'https://cdn.example.com/avatars/john.jpg'
};
const profile = new UserProfile(user);
document.getElementById('profile').appendChild(profile.render());
Best Practices
-
Use textContent for user content:
// โ Good element.textContent = userInput; // โ Bad element.innerHTML = userInput; -
Implement CSP headers:
// โ Good // Content-Security-Policy: default-src 'self' // โ Bad // No CSP headers -
Validate and sanitize:
// โ Good const sanitized = DOMPurify.sanitize(userHTML); // โ Bad const html = userHTML;
Common Mistakes
-
Using innerHTML with user input:
// โ Bad element.innerHTML = userInput; // โ Good element.textContent = userInput; -
Not implementing CSP:
// โ Bad - no CSP protection // โ Good // Content-Security-Policy: default-src 'self' -
Trusting client-side validation:
// โ Bad - only client validation if (validateInput(input)) { submitForm(); } // โ Good - validate server-side too
Summary
XSS prevention is critical for security. Key takeaways:
- Understand XSS types
- Encode output
- Validate input
- Implement CSP
- Use security libraries
- Sanitize HTML
- Use textContent
- Validate URLs
Related Resources
Next Steps
- Learn about CSRF Protection
- Explore Secure Coding Practices
- Study Input Validation
- Practice XSS prevention
- Implement CSP headers
Comments