Web Security Best Practices: XSS, CSRF, and Content Security Policy
Master web security with XSS prevention, CSRF protection, and Content Security Policy. Learn practical defense strategies with code examples and real-world techniques.
Table of Contents
- Introduction
- Understanding Web Security Threats
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- Content Security Policy (CSP)
- Additional Security Headers
- Secure Coding Practices
- Security Testing and Monitoring
- Real-World Security Implementation
- Conclusion
Introduction
Web security is not optional—it’s fundamental to building trustworthy applications that protect users and their data. In an era where cyberattacks are increasingly sophisticated and frequent, understanding common vulnerabilities and how to prevent them is essential for every web developer.
Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) are among the most common web security vulnerabilities, appearing in the OWASP Top 10 year after year. These attacks can lead to data theft, unauthorized actions, and complete account compromise. Content Security Policy (CSP) provides a powerful defense mechanism against XSS and other injection attacks, while proper CSRF protection ensures that actions can only be performed by authenticated users.
This comprehensive guide will teach you everything you need to know about web security best practices. You’ll learn how XSS attacks work and how to prevent them, understand CSRF vulnerabilities and implement proper protection, and master Content Security Policy to create multiple layers of defense. By the end, you’ll have the knowledge and tools to build secure web applications that protect both your users and your business.
Understanding Web Security Threats
Before diving into specific vulnerabilities, it’s important to understand the threat landscape and the principles of secure web development.
The Security Mindset
Security is not a feature you add at the end—it’s a fundamental aspect of every decision you make during development. Adopting a security-first mindset means:
- Never trusting user input: Always validate and sanitize data from users
- Defense in depth: Implement multiple layers of security
- Least privilege: Grant only the minimum permissions necessary
- Fail securely: When security checks fail, default to the most secure state
- Keep dependencies updated: Regularly update libraries and frameworks
Common Attack Vectors
Web applications face numerous attack vectors:
- Injection Attacks: XSS, SQL Injection, Command Injection
- Authentication Flaws: Weak passwords, session hijacking, CSRF
- Sensitive Data Exposure: Insecure storage, insufficient encryption
- Broken Access Control: Unauthorized access, privilege escalation
- Security Misconfiguration: Default credentials, exposed error messages
The Impact of Security Breaches
Security vulnerabilities can have severe consequences:
- Data Breaches: Exposure of personal information, financial data
- Financial Loss: Theft, fraud, regulatory fines
- Reputation Damage: Loss of user trust and business credibility
- Legal Liability: Compliance violations, lawsuits
- Service Disruption: DDoS attacks, data corruption
Understanding these threats motivates us to implement proper security measures. Let’s explore the most common vulnerabilities and how to prevent them.
Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) is one of the most prevalent web security vulnerabilities. It occurs when an attacker injects malicious scripts into web pages viewed by other users, allowing them to execute arbitrary JavaScript in the victim’s browser.
Types of XSS Attacks
There are three main types of XSS attacks:
1. Stored XSS (Persistent XSS) Malicious scripts are permanently stored on the target server (e.g., in a database, comment field, or user profile). Every user who views the infected page executes the malicious script.
2. Reflected XSS (Non-Persistent XSS) Malicious scripts are reflected off a web server, typically in error messages or search results. The attack is delivered via a URL that contains the malicious payload.
3. DOM-based XSS The vulnerability exists in client-side code rather than server-side code. The attack payload is executed as a result of modifying the DOM environment.
How XSS Attacks Work
Here’s a simple example of a vulnerable application:
// ❌ Vulnerable: Directly inserting user input into HTMLfunction displayComment(userComment) { document.getElementById("comments").innerHTML += `<div class="comment">${userComment}</div>`;}
// Attacker submits: <script>alert('XSS')</script>// Result: Script executes in victim's browserXSS Attack Examples
Example 1: Stored XSS in Comments
<!-- ❌ Vulnerable comment system --><div id="comments"> <!-- User comment stored in database: --> <div class="comment"> Great article! <script> stealCookies(); </script> </div></div>Example 2: Reflected XSS in Search
// ❌ Vulnerable search pageconst urlParams = new URLSearchParams(window.location.search);const query = urlParams.get("q");
// Directly inserting query into pagedocument.getElementById("results").innerHTML = `Search results for: ${query}`;
// Attacker URL: /search?q=<script>alert('XSS')</script>Example 3: DOM-based XSS
// ❌ Vulnerable: Using location.hash without sanitizationconst hash = window.location.hash.substring(1);document.getElementById("content").innerHTML = hash;
// Attacker URL: /page#<img src=x onerror=alert('XSS')>Preventing XSS Attacks
✅ Output Encoding
Always encode user input before displaying it:
// ✅ Good: HTML entity encodingfunction escapeHtml(text) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; return text.replace(/[&<>"']/g, (m) => map[m]);}
function displayComment(userComment) { const safeComment = escapeHtml(userComment); document.getElementById("comments").innerHTML += `<div class="comment">${safeComment}</div>`;}✅ Using Text Nodes Instead of innerHTML
// ✅ Good: Using textContent prevents HTML injectionfunction displayComment(userComment) { const commentDiv = document.createElement("div"); commentDiv.className = "comment"; commentDiv.textContent = userComment; // Safe: treats input as text document.getElementById("comments").appendChild(commentDiv);}✅ Using Safe DOM APIs
// ✅ Good: Using createElement and textContentfunction createUserLink(username) { const link = document.createElement("a"); link.href = `/users/${encodeURIComponent(username)}`; link.textContent = username; // Safe return link;}
// ❌ Bad: Using innerHTML with user inputfunction createUserLink(username) { return `<a href="/users/${username}">${username}</a>`;}✅ Server-Side Validation and Sanitization
// ✅ Good: Server-side sanitization (Node.js example)const DOMPurify = require("isomorphic-dompurify");
app.post("/comments", (req, res) => { const comment = DOMPurify.sanitize(req.body.comment, { ALLOWED_TAGS: [], // No HTML tags allowed ALLOWED_ATTR: [], });
// Store sanitized comment db.comments.create({ content: comment });});✅ Using Template Literals Safely
// ✅ Good: Using template literals with proper escapingfunction createUserCard(user) { return ` <div class="user-card"> <h2>${escapeHtml(user.name)}</h2> <p>${escapeHtml(user.bio)}</p> </div> `;}React and XSS Prevention
React provides built-in XSS protection by default:
// ✅ Good: React automatically escapes contentfunction Comment({ text }) { return <div className="comment">{text}</div>; // React automatically escapes 'text' - safe!}
// ⚠️ Dangerous: Using dangerouslySetInnerHTMLfunction Comment({ htmlContent }) { return ( <div className="comment" dangerouslySetInnerHTML={{ __html: htmlContent }} /> ); // Only use if you've sanitized htmlContent server-side!}✅ Sanitizing HTML in React
import DOMPurify from "dompurify";
function SafeHTML({ html }) { // ✅ Sanitize before rendering const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p"], ALLOWED_ATTR: ["href"], });
return <div dangerouslySetInnerHTML={{ __html: clean }} />;}XSS Prevention Checklist
✅ Always validate and sanitize user input on the server ✅ Use parameterized queries for database operations ✅ Encode output based on context (HTML, JavaScript, URL, CSS) ✅ Use framework features that prevent XSS (React, Vue, Angular) ✅ Implement Content Security Policy (CSP) ✅ Use HTTP-only cookies for sensitive data ✅ Regularly update dependencies to patch vulnerabilities
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to execute unwanted actions on a web application. The attack exploits the trust that a site has in a user’s browser.
How CSRF Attacks Work
CSRF attacks work by tricking a logged-in user into making a request to a vulnerable application. Here’s a typical attack scenario:
- User logs into
bank.comand receives a session cookie - User visits
evil.com(while still logged intobank.com) evil.comcontains a form that submits tobank.com/transfer- Browser automatically includes the session cookie with the request
- Bank processes the transfer as if the user initiated it
CSRF Attack Example
<!-- ❌ Vulnerable: No CSRF protection --><!-- bank.com/transfer endpoint --><form method="POST" action="https://bank.com/transfer"> <input type="hidden" name="to" value="attacker-account" /> <input type="hidden" name="amount" value="1000" /> <button type="submit">Click for free money!</button></form>
<!-- Attacker can embed this on evil.com --><!-- When user clicks, transfer happens automatically -->Preventing CSRF Attacks
✅ CSRF Tokens
The most common defense is using CSRF tokens—unique, unpredictable values that are included in forms and validated on the server.
// ✅ Server-side: Generate and validate CSRF tokensconst crypto = require("crypto");const sessions = new Map();
// Generate CSRF tokenfunction generateCSRFToken(sessionId) { const token = crypto.randomBytes(32).toString("hex"); sessions.get(sessionId).csrfToken = token; return token;}
// Validate CSRF tokenfunction validateCSRFToken(sessionId, token) { const session = sessions.get(sessionId); if (!session || session.csrfToken !== token) { throw new Error("Invalid CSRF token"); } return true;}
// Express.js middleware exampleapp.use((req, res, next) => { if (!req.session.csrfToken) { req.session.csrfToken = generateCSRFToken(req.session.id); } res.locals.csrfToken = req.session.csrfToken; next();});
// Protect POST routesapp.post("/transfer", (req, res) => { validateCSRFToken(req.session.id, req.body.csrfToken); // Process transfer...});✅ Including CSRF Token in Forms
<!-- ✅ Good: Include CSRF token in form --><form method="POST" action="/transfer"> <input type="hidden" name="csrfToken" value="{{ csrfToken }}" /> <input type="text" name="to" placeholder="Recipient" /> <input type="number" name="amount" placeholder="Amount" /> <button type="submit">Transfer</button></form>✅ AJAX Requests with CSRF Tokens
// ✅ Good: Include CSRF token in AJAX requestsfunction makeTransfer(to, amount) { fetch("/transfer", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": getCSRFToken(), // Get from meta tag or cookie }, body: JSON.stringify({ to, amount }), });}
// Get CSRF token from meta tagfunction getCSRFToken() { return document .querySelector('meta[name="csrf-token"]') .getAttribute("content");}✅ SameSite Cookie Attribute
The SameSite cookie attribute provides CSRF protection by preventing cookies from being sent in cross-site requests.
// ✅ Good: Set SameSite attribute on cookiesapp.use( session({ cookie: { httpOnly: true, secure: true, // HTTPS only sameSite: "strict", // Prevents CSRF }, }),);SameSite Values:
strict: Cookie never sent in cross-site requestslax: Cookie sent in top-level navigation (default in modern browsers)none: Cookie sent in all contexts (requiressecureflag)
Double Submit Cookie Pattern
An alternative to server-side token storage is the double submit cookie pattern:
// ✅ Generate token and set as both cookie and form valuefunction setCSRFToken(res, token) { res.cookie("csrf-token", token, { httpOnly: false, // Must be accessible to JavaScript secure: true, sameSite: "strict", }); return token;}
// Validate: token in cookie matches token in formfunction validateDoubleSubmit(req) { const cookieToken = req.cookies["csrf-token"]; const formToken = req.body.csrfToken; return cookieToken && cookieToken === formToken;}CSRF Protection in Modern Frameworks
Express.js with csurf:
const csrf = require("csurf");const csrfProtection = csrf({ cookie: true });
// Apply to routes that need protectionapp.post("/transfer", csrfProtection, (req, res) => { // req.csrfToken() available for forms res.render("transfer", { csrfToken: req.csrfToken() });});Laravel CSRF Protection:
// Laravel automatically includes CSRF protection// In Blade template:<form method="POST" action="/transfer"> @csrf <input type="text" name="to" placeholder="Recipient"> <input type="number" name="amount" placeholder="Amount"> <button type="submit">Transfer</button></form>
// In controller:namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TransferController extends Controller{ public function transfer(Request $request) { // CSRF token validated automatically by Laravel middleware // Process transfer... $request->validate([ 'to' => 'required|string', 'amount' => 'required|numeric|min:1' ]);
// Transfer logic here }}
// For AJAX requests, include CSRF token in meta tag:// <meta name="csrf-token" content="{{ csrf_token() }}">// Then in JavaScript:// headers: {// 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content// }CSRF Prevention Checklist
✅ Use CSRF tokens for state-changing operations
✅ Validate CSRF tokens on the server
✅ Use SameSite cookie attribute
✅ Implement double submit cookie pattern if needed
✅ Only allow safe methods (GET, HEAD, OPTIONS) without CSRF protection
✅ Use framework-provided CSRF protection when available
✅ Regenerate CSRF tokens after login
✅ Consider using custom headers for AJAX requests
Content Security Policy (CSP)
Content Security Policy (CSP) is a security standard that helps prevent XSS, data injection attacks, and other code injection vulnerabilities. CSP allows you to specify which sources of content are trusted and allowed to execute.
How CSP Works
CSP works by defining a whitelist of trusted sources for various types of content. When a browser encounters content from an untrusted source, it blocks it according to the policy.
Basic CSP Syntax
CSP is implemented via HTTP headers or meta tags:
<!-- ✅ Meta tag approach --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';"/>
<!-- Or via HTTP header -->Content-Security-Policy: default-src 'self'; script-src 'self'CSP Directives
Common CSP directives include:
default-src: Fallback for other fetch directivesscript-src: Controls which scripts can executestyle-src: Controls which stylesheets can applyimg-src: Controls which images can loadfont-src: Controls which fonts can loadconnect-src: Controls which URLs can be loaded via fetch/XHRframe-src: Controls which URLs can be embedded as framesobject-src: Controls plugins (Flash, etc.)base-uri: Controls the base element’s URLform-action: Controls form submission URLsframe-ancestors: Controls which sites can embed your page
CSP Source Values
'self': Same origin as the document'unsafe-inline': Allows inline scripts/styles (not recommended)'unsafe-eval': Allows eval() and similar functions (not recommended)'none': Blocks all sourceshttps:orhttps://example.com: Specific protocol or domain'nonce-{value}': Allows specific inline scripts with matching nonce'sha256-{hash}': Allows inline scripts/styles with matching hash
Implementing CSP
✅ Strict CSP Policy
// ✅ Good: Strict CSP policyapp.use((req, res, next) => { res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' 'nonce-{random}'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self' data:; " + "connect-src 'self' https://api.example.com; " + "frame-ancestors 'none';", ); next();});✅ Using Nonces for Inline Scripts
const crypto = require('crypto');
// Generate nonce for each requestapp.use((req, res, next) => { res.locals.nonce = crypto.randomBytes(16).toString('base64'); res.setHeader('Content-Security-Policy', `script-src 'self' 'nonce-${res.locals.nonce}';` ); next();});
// In template<script nonce="{{ nonce }}"> // This script will execute</script>
<script> // This script will be blocked</script>✅ Using Hashes for Inline Scripts
const crypto = require("crypto");
// Calculate hash of inline scriptconst script = "console.log('Hello World');";const hash = crypto.createHash("sha256").update(script).digest("base64");
// Set CSP with hashres.setHeader("Content-Security-Policy", `script-src 'self' 'sha256-${hash}';`);CSP for Common Use Cases
✅ Allowing CDN Resources
res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' https://cdn.jsdelivr.net https://unpkg.com; " + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + "font-src 'self' https://fonts.googleapis.com; " + "img-src 'self' data: https:;",);✅ Allowing Google Analytics
res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; " + "connect-src 'self' https://www.google-analytics.com; " + "img-src 'self' data: https://www.google-analytics.com;",);✅ Allowing React Development
// Development: Allow eval for hot reloadingif (process.env.NODE_ENV === "development") { res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' 'unsafe-eval' 'unsafe-inline';", );} else { // Production: Strict policy res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self'; " + "style-src 'self' 'unsafe-inline';", );}Reporting CSP Violations
CSP can report violations to a specified endpoint:
res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "report-uri /csp-violation-report;",);
// Handle violation reportsapp.post("/csp-violation-report", express.json(), (req, res) => { console.log("CSP Violation:", req.body); // Log to monitoring service res.status(204).send();});✅ Using report-to Directive (Modern)
res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "report-to csp-endpoint;",);
res.setHeader( "Report-To", JSON.stringify({ group: "csp-endpoint", max_age: 10886400, endpoints: [{ url: "/csp-violation-report" }], }),);CSP Best Practices
✅ Start Strict, Relax Gradually
Begin with a strict policy and gradually relax it based on violation reports:
// ✅ Start with report-only moderes.setHeader( "Content-Security-Policy-Report-Only", "default-src 'self'; " + "report-uri /csp-violation-report;",);
// Monitor violations, then switch to enforce moderes.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' https://trusted-cdn.com;",);✅ Use Nonces Over Unsafe-Inline
// ❌ Bad: Using unsafe-inline"script-src 'self' 'unsafe-inline';";
// ✅ Good: Using nonces"script-src 'self' 'nonce-{random}';";✅ Separate Policies for Different Pages
Different pages may need different CSP policies:
// Public pages: More restrictiveapp.get("/public/*", (req, res, next) => { res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self';", ); next();});
// Admin pages: Allow more sourcesapp.get("/admin/*", (req, res, next) => { res.setHeader( "Content-Security-Policy", "default-src 'self'; " + "script-src 'self' https://admin-tools.com;", ); next();});CSP Checklist
✅ Implement CSP headers on all pages
✅ Start with Content-Security-Policy-Report-Only mode
✅ Use nonces or hashes instead of unsafe-inline
✅ Avoid unsafe-eval in production
✅ Monitor CSP violation reports
✅ Test CSP policies in staging before production
✅ Keep CSP policies as strict as possible
✅ Document allowed sources and why they’re needed
Additional Security Headers
Beyond CSP, several other HTTP security headers provide additional protection layers.
Security Headers Overview
✅ X-Content-Type-Options
Prevents MIME type sniffing:
res.setHeader("X-Content-Type-Options", "nosniff");✅ X-Frame-Options
Prevents clickjacking attacks:
// Prevent embedding in framesres.setHeader("X-Frame-Options", "DENY");
// Or allow only same originres.setHeader("X-Frame-Options", "SAMEORIGIN");✅ X-XSS-Protection
Enables browser’s built-in XSS filter (legacy, CSP is preferred):
res.setHeader("X-XSS-Protection", "1; mode=block");✅ Strict-Transport-Security (HSTS)
Forces HTTPS connections:
res.setHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload",);✅ Referrer-Policy
Controls referrer information:
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");✅ Permissions-Policy
Controls browser features:
res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");Complete Security Headers Setup
// ✅ Comprehensive security headers middlewarefunction securityHeaders(req, res, next) { // Prevent MIME sniffing res.setHeader("X-Content-Type-Options", "nosniff");
// Prevent clickjacking res.setHeader("X-Frame-Options", "DENY");
// XSS protection (legacy) res.setHeader("X-XSS-Protection", "1; mode=block");
// Force HTTPS res.setHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload", );
// Control referrer res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// Control browser features res.setHeader( "Permissions-Policy", "geolocation=(), microphone=(), camera=()", );
// CSP (set separately based on page) // Content-Security-Policy header set elsewhere
next();}
app.use(securityHeaders);Secure Coding Practices
Beyond specific vulnerabilities, following secure coding practices creates a foundation for secure applications.
Input Validation
✅ Validate All Input
// ✅ Good: Validate input type and formatfunction validateEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new Error("Invalid email format"); } return email.toLowerCase().trim();}
function validateAmount(amount) { const num = parseFloat(amount); if (isNaN(num) || num <= 0 || num > 10000) { throw new Error("Invalid amount"); } return num;}✅ Use Parameterized Queries
// ✅ Good: Parameterized queries prevent SQL injectionconst db = require("./db");
async function getUser(username) { // ✅ Safe: Parameters are escaped automatically const result = await db.query("SELECT * FROM users WHERE username = $1", [ username, ]); return result.rows[0];}
// ❌ Bad: String concatenation vulnerable to SQL injectionasync function getUser(username) { const result = await db.query( `SELECT * FROM users WHERE username = '${username}'`, ); return result.rows[0];}Authentication and Session Management
✅ Secure Password Storage
const bcrypt = require("bcrypt");
// ✅ Hash passwords with bcryptasync function hashPassword(password) { const saltRounds = 12; return await bcrypt.hash(password, saltRounds);}
// ✅ Verify passwordsasync function verifyPassword(password, hash) { return await bcrypt.compare(password, hash);}✅ Secure Session Management
// ✅ Good: Secure session configurationapp.use( session({ secret: process.env.SESSION_SECRET, // Strong random secret resave: false, saveUninitialized: false, cookie: { httpOnly: true, // Prevents JavaScript access secure: true, // HTTPS only sameSite: "strict", // CSRF protection maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }),);Error Handling
✅ Don’t Expose Sensitive Information
// ❌ Bad: Exposing database errorsapp.use((err, req, res, next) => { res.status(500).json({ error: err.message, // May contain sensitive info });});
// ✅ Good: Generic error messagesapp.use((err, req, res, next) => { console.error(err); // Log full error server-side res.status(500).json({ error: "An error occurred. Please try again later.", });});Dependency Management
✅ Keep Dependencies Updated
# ✅ Check for vulnerabilitiespnpm audit
# ✅ Fix vulnerabilitiespnpm audit fix
# ✅ Update dependencies regularlypnpm update✅ Use Dependency Scanning
{ "scripts": { "security": "npm audit && snyk test" }}Security Testing and Monitoring
Regular security testing and monitoring help identify vulnerabilities before attackers do.
Security Testing Tools
✅ Static Analysis
- ESLint security plugins
- SonarQube
- Semgrep
✅ Dependency Scanning
pnpm audit- Snyk
- Dependabot
✅ Dynamic Analysis
- OWASP ZAP
- Burp Suite
- Browser DevTools Security tab
Security Monitoring
✅ Log Security Events
// ✅ Log authentication attemptsfunction logAuthAttempt(username, success, ip) { logger.info("Auth attempt", { username, success, ip, timestamp: new Date().toISOString(), });
if (!success) { // Alert on repeated failures checkBruteForce(username, ip); }}✅ Monitor CSP Violations
app.post("/csp-violation-report", express.json(), (req, res) => { const violation = req.body["csp-report"];
// Log violation logger.warn("CSP violation", { documentUri: violation["document-uri"], violatedDirective: violation["violated-directive"], blockedUri: violation["blocked-uri"], });
// Alert if critical if (violation["violated-directive"].includes("script-src")) { alertSecurityTeam(violation); }
res.status(204).send();});Security Headers Testing
Test your security headers using online tools:
- SecurityHeaders.com
- Mozilla Observatory
- Browser DevTools Network tab
Real-World Security Implementation
Let’s put it all together with a complete security implementation example.
Complete Security Middleware
const helmet = require("helmet");const rateLimit = require("express-rate-limit");const crypto = require("crypto");
// Generate nonce for CSPfunction generateNonce() { return crypto.randomBytes(16).toString("base64");}
// Security middlewarefunction setupSecurity(app) { // Helmet sets various security headers app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'", "data:"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, }), );
// Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs }); app.use("/api/", limiter);
// Generate nonce for each request app.use((req, res, next) => { res.locals.nonce = generateNonce(); next(); });}
module.exports = { setupSecurity };Secure Express Application
const express = require("express");const session = require("express-session");const { setupSecurity } = require("./security");const csrf = require("csurf");
const app = express();
// Security setupsetupSecurity(app);
// Session configurationapp.use( session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, }, }),);
// CSRF protectionconst csrfProtection = csrf({ cookie: true });app.use(csrfProtection);
// Routesapp.get("/", (req, res) => { res.render("index", { csrfToken: req.csrfToken(), nonce: res.locals.nonce, });});
app.post("/transfer", (req, res) => { // CSRF token validated automatically // Process transfer... res.json({ success: true });});
app.listen(3000);Secure Frontend Template
<!-- index.ejs --><!DOCTYPE html><html> <head> <meta name="csrf-token" content="<%= csrfToken %>" /> </head> <body> <form id="transfer-form"> <input type="hidden" name="csrfToken" value="<%= csrfToken %>" /> <input type="text" name="to" placeholder="Recipient" /> <input type="number" name="amount" placeholder="Amount" /> <button type="submit">Transfer</button> </form>
<!-- ✅ Inline script with nonce --> <script nonce="<%= nonce %>"> // Safe inline script document .getElementById("transfer-form") .addEventListener("submit", async (e) => { e.preventDefault(); const formData = new FormData(e.target);
const response = await fetch("/transfer", { method: "POST", headers: { "X-CSRF-Token": document .querySelector('meta[name="csrf-token"]') .getAttribute("content"), }, body: formData, });
const result = await response.json(); console.log(result); }); </script> </body></html>Conclusion
Web security is an ongoing process that requires vigilance, knowledge, and the right tools. XSS, CSRF, and other vulnerabilities pose real threats to applications and users, but with proper understanding and implementation of security best practices, you can build robust defenses.
Key takeaways from this guide:
- XSS Prevention: Always validate and sanitize user input, use output encoding, and leverage framework protections
- CSRF Protection: Implement CSRF tokens, use SameSite cookies, and protect all state-changing operations
- Content Security Policy: Use CSP to create multiple layers of defense, start with report-only mode, and use nonces instead of unsafe-inline
- Security Headers: Implement comprehensive security headers to protect against various attack vectors
- Secure Coding: Follow secure coding practices, validate input, use parameterized queries, and handle errors securely
- Testing and Monitoring: Regularly test for vulnerabilities, monitor security events, and keep dependencies updated
Remember that security is not a one-time task—it’s an ongoing commitment. Regular security audits, dependency updates, and monitoring are essential for maintaining a secure application. Start with the basics, implement these practices incrementally, and continuously improve your security posture.
For more on building secure and performant web applications, check out our guide on web performance optimization and our on-page SEO best practices which also cover security-related aspects of web development.
By implementing these security measures, you’re not just protecting your application—you’re protecting your users, your business, and your reputation. Stay secure, stay vigilant, and keep learning.