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