Web Security Fundamentals: XSS, CSRF, SQL Injection & Modern Defenses
Rating
Verdict
Web security is not optional. Implement parameterized queries, CSP headers, CSRF protection, and rate limiting from day one. Run npm audit regularly and subscribe to security advisories for your dependencies.
Pros
- Understanding attacks makes prevention intuitive
- CSP headers eliminate entire classes of XSS
- Parameterized queries completely prevent SQL injection
- Security headers are zero-cost protections
- Most vulnerabilities are preventable with basic hygiene
Cons
- Security is a continuous process, not a checkbox
- Third-party dependencies can introduce vulnerabilities
- Social engineering bypasses technical controls
Web Security Fundamentals: XSS, CSRF, SQL Injection & Modern Defenses
Web application security is not a feature you add at the end — it's a mindset you maintain throughout development. The consequences of getting it wrong range from embarrassing to catastrophic: data breaches, account takeovers, financial fraud. This guide covers the most critical vulnerabilities and their practical defenses.
Cross-Site Scripting (XSS)
XSS occurs when untrusted data is included in web pages without proper escaping, allowing attackers to inject malicious scripts. There are three types: Stored XSS, Reflected XSS, and DOM-based XSS.
// ✗ VULNERABLE — rendering user input as HTML
app.get('/search', (req, res) => {
res.send(`
Results for: ${req.query.q}
`);
// Attack: /search?q=
});
// ✓ SAFE — escape output
import { escapeHtml } from './utils';
app.get('/search', (req, res) => {
res.send(`Results for: ${escapeHtml(req.query.q as string)}
`);
});
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Content Security Policy (CSP)
CSP is the strongest XSS defense — it tells browsers which sources are trusted for scripts, styles, and other resources:
// Express — set CSP headers
import helmet from 'helmet';
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'nonce-{RANDOM_NONCE}'", // Inline scripts need a nonce
"https://cdn.trusted.com",
],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https://res.cloudinary.com"],
connectSrc: ["'self'", "https://api.myapp.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
})
);
// Next.js — CSP in middleware
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request: Request) {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\n/g, '');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Nonce', nonce);
return response;
}
SQL Injection Prevention
SQL injection is completely preventable with parameterized queries. Never concatenate user input into SQL strings:
// ✗ VULNERABLE — string concatenation
const username = req.body.username;
const query = `SELECT * FROM users WHERE username = '${username}'`;
// Attack: username = "' OR '1'='1" — returns ALL users!
// ✓ SAFE — parameterized query (node-postgres)
const { rows } = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username]
);
// ✓ SAFE — Prisma (always parameterized)
const user = await prisma.user.findUnique({
where: { username }, // Never vulnerable
});
// ✓ SAFE — raw query with Prisma (still parameterized)
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE username = ${username}
`;
// DO NOT use prisma.$queryRawUnsafe with user input!
CSRF Protection
Cross-Site Request Forgery tricks authenticated users into submitting requests to your app from a malicious site:
// Server: generate and validate CSRF tokens
import crypto from 'crypto';
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Store token in session, send to client
app.get('/api/csrf-token', (req, res) => {
const token = generateCsrfToken();
req.session.csrfToken = token;
res.json({ token });
});
// Validate on state-changing requests
function validateCsrf(req: Request, res: Response, next: NextFunction) {
const token = req.headers['x-csrf-token'] || req.body._csrf;
if (!token || !req.session.csrfToken || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
}
app.post('/api/profile', authenticate, validateCsrf, updateProfile);
// Client: include CSRF token in all mutating requests
async function apiFetch(url: string, options: RequestInit = {}) {
const tokenRes = await fetch('/api/csrf-token');
const { token } = await tokenRes.json();
return fetch(url, {
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
'X-CSRF-Token': token,
},
credentials: 'include',
});
}
Authentication Hardening
// Strong password hashing with bcrypt
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12; // Recommended: 10-14
async function hashPassword(password: string): Promise
Essential Security Headers
app.use(helmet({
// Prevent clickjacking
frameguard: { action: 'deny' },
// Force HTTPS for 1 year
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
// Prevent MIME type sniffing
noSniff: true,
// Referrer policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Disable X-Powered-By
hidePoweredBy: true,
// Permissions Policy
permittedCrossDomainPolicies: false,
}));
// Additional headers
app.use((_, res, next) => {
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
Dependency Security
# Audit dependencies
npm audit
npm audit fix
# Check for known vulnerabilities automatically in CI
npm audit --audit-level=high # Fail CI if high/critical vulns found
# Update all deps to latest compatible versions
npx npm-check-updates -u
npm install
# Use Socket.dev for supply chain security
npx @socketregistry/cli check
Conclusion
Web security is a continuous discipline, not a one-time task. The defenses covered here — parameterized queries, CSP, CSRF tokens, security headers, bcrypt password hashing, and JWT best practices — address the vast majority of real-world web vulnerabilities. Implement them from the start of every project, keep dependencies updated, and run regular security audits. Security debt compounds just like technical debt.