Security

Web Security Fundamentals: XSS, CSRF, SQL Injection & Modern Defenses

A
Admin
March 9, 2026 • 5 min read • 875 words
9
Overall Scoreout of 10

Rating

9/10

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 { return bcrypt.hash(password, SALT_ROUNDS); } async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } // Password strength validation import { z } from 'zod'; const PasswordSchema = z.string() .min(8, 'At least 8 characters') .regex(/[A-Z]/, 'At least one uppercase letter') .regex(/[a-z]/, 'At least one lowercase letter') .regex(/[0-9]/, 'At least one number') .regex(/[^A-Za-z0-9]/, 'At least one special character'); // Secure JWT configuration import jwt from 'jsonwebtoken'; function signToken(userId: string): string { return jwt.sign( { userId, iat: Math.floor(Date.now() / 1000) }, process.env.JWT_SECRET!, { expiresIn: '1h', // Short-lived access tokens algorithm: 'HS256', issuer: 'myapp.com', audience: 'myapp-users', } ); }

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.

#Security#XSS#CSRF#SQL Injection#Web Development#OWASP
Web Security Fundamentals: XSS, CSRF, SQL Injection & Modern Defenses | Pulse