Skip to main content

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

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:

  1. Injection Attacks: XSS, SQL Injection, Command Injection
  2. Authentication Flaws: Weak passwords, session hijacking, CSRF
  3. Sensitive Data Exposure: Insecure storage, insufficient encryption
  4. Broken Access Control: Unauthorized access, privilege escalation
  5. 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 HTML
function displayComment(userComment) {
document.getElementById("comments").innerHTML +=
`<div class="comment">${userComment}</div>`;
}
// Attacker submits: <script>alert('XSS')</script>
// Result: Script executes in victim's browser

XSS 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 page
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get("q");
// Directly inserting query into page
document.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 sanitization
const 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 encoding
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
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 injection
function 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 textContent
function createUserLink(username) {
const link = document.createElement("a");
link.href = `/users/${encodeURIComponent(username)}`;
link.textContent = username; // Safe
return link;
}
// ❌ Bad: Using innerHTML with user input
function 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 escaping
function 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 content
function Comment({ text }) {
return <div className="comment">{text}</div>;
// React automatically escapes 'text' - safe!
}
// ⚠️ Dangerous: Using dangerouslySetInnerHTML
function 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:

  1. User logs into bank.com and receives a session cookie
  2. User visits evil.com (while still logged into bank.com)
  3. evil.com contains a form that submits to bank.com/transfer
  4. Browser automatically includes the session cookie with the request
  5. 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 tokens
const crypto = require("crypto");
const sessions = new Map();
// Generate CSRF token
function generateCSRFToken(sessionId) {
const token = crypto.randomBytes(32).toString("hex");
sessions.get(sessionId).csrfToken = token;
return token;
}
// Validate CSRF token
function validateCSRFToken(sessionId, token) {
const session = sessions.get(sessionId);
if (!session || session.csrfToken !== token) {
throw new Error("Invalid CSRF token");
}
return true;
}
// Express.js middleware example
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken(req.session.id);
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// Protect POST routes
app.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 requests
function 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 tag
function 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 cookies
app.use(
session({
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: "strict", // Prevents CSRF
},
}),
);

SameSite Values:

  • strict: Cookie never sent in cross-site requests
  • lax: Cookie sent in top-level navigation (default in modern browsers)
  • none: Cookie sent in all contexts (requires secure flag)

An alternative to server-side token storage is the double submit cookie pattern:

// ✅ Generate token and set as both cookie and form value
function 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 form
function 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 protection
app.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 directives
  • script-src: Controls which scripts can execute
  • style-src: Controls which stylesheets can apply
  • img-src: Controls which images can load
  • font-src: Controls which fonts can load
  • connect-src: Controls which URLs can be loaded via fetch/XHR
  • frame-src: Controls which URLs can be embedded as frames
  • object-src: Controls plugins (Flash, etc.)
  • base-uri: Controls the base element’s URL
  • form-action: Controls form submission URLs
  • frame-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 sources
  • https: or https://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 policy
app.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 request
app.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 script
const script = "console.log('Hello World');";
const hash = crypto.createHash("sha256").update(script).digest("base64");
// Set CSP with hash
res.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 reloading
if (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 reports
app.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 mode
res.setHeader(
"Content-Security-Policy-Report-Only",
"default-src 'self'; " + "report-uri /csp-violation-report;",
);
// Monitor violations, then switch to enforce mode
res.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 restrictive
app.get("/public/*", (req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; " + "script-src 'self';",
);
next();
});
// Admin pages: Allow more sources
app.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 frames
res.setHeader("X-Frame-Options", "DENY");
// Or allow only same origin
res.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 middleware
function 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 format
function 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 injection
const 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 injection
async 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 bcrypt
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
// ✅ Verify passwords
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}

✅ Secure Session Management

// ✅ Good: Secure session configuration
app.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 errors
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message, // May contain sensitive info
});
});
// ✅ Good: Generic error messages
app.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

Terminal window
# ✅ Check for vulnerabilities
pnpm audit
# ✅ Fix vulnerabilities
pnpm audit fix
# ✅ Update dependencies regularly
pnpm update

✅ Use Dependency Scanning

package.json
{
"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 attempts
function 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:


Real-World Security Implementation

Let’s put it all together with a complete security implementation example.

Complete Security Middleware

security.js
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const crypto = require("crypto");
// Generate nonce for CSP
function generateNonce() {
return crypto.randomBytes(16).toString("base64");
}
// Security middleware
function 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

app.js
const express = require("express");
const session = require("express-session");
const { setupSecurity } = require("./security");
const csrf = require("csurf");
const app = express();
// Security setup
setupSecurity(app);
// Session configuration
app.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 protection
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
// Routes
app.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.