Web Application Security
Introduction
Web application security focuses on protecting websites and online services against security threats that exploit vulnerabilities in an application's code. This guide covers essential security measures and best practices for developing secure web applications.
Security Pillars:
- Input Validation
- Output Encoding
- Authentication
- Session Management
- Access Control
- Error Handling
Injection Prevention
SQL Injection Prevention
import { Pool } from 'pg';
class DatabaseService {
private pool: Pool;
constructor() {
this.pool = new Pool({
connectionString: process.env.DATABASE_URL
});
}
// Bad: Vulnerable to SQL injection
async unsafeQuery(username: string): Promise {
const query = `SELECT * FROM users WHERE username = '${username}'`;
return await this.pool.query(query);
}
// Good: Safe from SQL injection
async safeQuery(username: string): Promise {
const query = 'SELECT * FROM users WHERE username = $1';
return await this.pool.query(query, [username]);
}
// Good: Safe batch operations
async safeBatchInsert(users: any[]): Promise {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const query = 'INSERT INTO users (username, email, role) VALUES ($1, $2, $3)';
for (const user of users) {
await client.query(query, [user.username, user.email, user.role]);
}
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
}
NoSQL Injection Prevention
import { MongoClient, ObjectId } from 'mongodb';
class MongoService {
private client: MongoClient;
constructor(uri: string) {
this.client = new MongoClient(uri);
}
// Bad: Vulnerable to NoSQL injection
async unsafeFindUser(username: string): Promise {
const collection = this.client.db().collection('users');
return await collection.findOne({
$where: `this.username === '${username}'`
});
}
// Good: Safe from NoSQL injection
async safeFindUser(username: string): Promise {
const collection = this.client.db().collection('users');
return await collection.findOne({ username });
}
// Good: Safe query builder
buildQuery(filters: Record): Record {
const safeFilters: Record = {};
for (const [key, value] of Object.entries(filters)) {
if (typeof value === 'string') {
safeFilters[key] = value;
} else if (value instanceof ObjectId) {
safeFilters[key] = value;
} else if (Array.isArray(value)) {
safeFilters[key] = { $in: value };
}
}
return safeFilters;
}
}
XSS Protection
Input Sanitization
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
class XSSProtection {
private readonly window: any;
private readonly purify: any;
constructor() {
this.window = new JSDOM('').window;
this.purify = DOMPurify(this.window);
}
sanitizeHTML(input: string): string {
return this.purify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
}
encodeHTML(input: string): string {
return input
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
validateInput(input: string): boolean {
const dangerousPatterns = [
/script\b[^>]*>[\s\S]*?<\/script/gi,
/javascript:/gi,
/on\w+=/gi,
/data:/gi
];
return !dangerousPatterns.some(pattern => pattern.test(input));
}
}
CSRF Defense
CSRF Token Implementation
import { randomBytes, timingSafeEqual } from 'crypto';
class CSRFProtection {
private readonly tokenLength: number = 32;
private readonly tokenExpiry: number = 3600; // 1 hour
private readonly tokens: Map;
constructor() {
this.tokens = new Map();
}
generateToken(sessionId: string): string {
const token = randomBytes(this.tokenLength);
const expires = Date.now() + (this.tokenExpiry * 1000);
this.tokens.set(sessionId, { value: token, expires });
return token.toString('hex');
}
verifyToken(sessionId: string, token: string): boolean {
const storedToken = this.tokens.get(sessionId);
if (!storedToken) {
return false;
}
if (Date.now() > storedToken.expires) {
this.tokens.delete(sessionId);
return false;
}
try {
const providedToken = Buffer.from(token, 'hex');
return timingSafeEqual(providedToken, storedToken.value);
} catch {
return false;
}
}
cleanupExpiredTokens(): void {
const now = Date.now();
for (const [sessionId, token] of this.tokens.entries()) {
if (now > token.expires) {
this.tokens.delete(sessionId);
}
}
}
}
Security Headers
Header Configuration
import { Request, Response, NextFunction } from 'express';
class SecurityHeaders {
apply(req: Request, res: Response, next: NextFunction): void {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Enable XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Control resource loading
res.setHeader('Content-Security-Policy', this.getCSP());
// HSTS (force HTTPS)
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Referrer policy
res.setHeader(
'Referrer-Policy',
'strict-origin-when-cross-origin'
);
next();
}
private getCSP(): string {
return [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'self'"
].join('; ');
}
}
Best Practices
Input Validation:
- Validate on both client and server
- Whitelist allowed characters
- Enforce length limits
- Type checking
- Format validation
- Content validation
Output Encoding:
- Context-specific encoding
- HTML encoding
- URL encoding
- JavaScript encoding
- SQL escaping
- CSV injection prevention
Error Handling:
- Generic error messages
- Detailed logging
- Custom error pages
- Proper status codes
- Graceful failure
- Debug info protection