Web application security is critical in today's threat landscape. This comprehensive guide covers essential security practices, from authentication and encryption to input validation and secure deployment strategies.
Defense in Depth Strategy
Implement multiple layers of security controls. If one layer fails, others provide protection:
- Network Layer: Firewalls, DDoS protection, WAF
- Application Layer: Input validation, output encoding, authentication
- Data Layer: Encryption at rest and in transit
- Infrastructure Layer: Secure configurations, regular updates
HTTPS and TLS Configuration
Always use HTTPS for all communications. Configure TLS properly:
Strong TLS Configuration
// Nginx TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Certificate pinning
add_header Public-Key-Pins 'pin-sha256="..."; max-age=2592000' always;
Input Validation and Sanitization
Never trust user input. Validate and sanitize all data:
Server-Side Validation
// Using Zod for validation
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(18).max(120),
phone: z.string().regex(/^+?[1-9]\d{1,14}$/),
});
async function createUser(req: Request) {
const body = await req.json();
// Validate input
const result = userSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error },
{ status: 400 }
);
}
// Use validated data
const user = await db.users.create({ data: result.data });
return Response.json(user);
}
Output Encoding
// Prevent XSS with output encoding
import DOMPurify from 'isomorphic-dompurify';
// For HTML content
const cleanHTML = DOMPurify.sanitize(userInput);
// For URLs
function sanitizeURL(url: string): string {
try {
const parsed = new URL(url);
// Only allow http/https
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Invalid protocol');
}
return parsed.toString();
} catch {
return '#';
}
}
// For JavaScript contexts
function escapeJS(str: string): string {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}
SQL Injection Prevention
Always use parameterized queries or ORMs:
// β VULNERABLE - Never do this
const query = `SELECT * FROM users WHERE email = '${email}'`;
db.query(query);
// β
SAFE - Use parameterized queries
const query = 'SELECT * FROM users WHERE email = $1';
db.query(query, [email]);
// β
SAFE - Use ORM (Prisma)
const user = await prisma.user.findUnique({
where: { email },
});
// β
SAFE - TypeORM
const user = await userRepository.findOne({
where: { email },
});
Cross-Site Scripting (XSS) Prevention
Content Security Policy (CSP)
// Strict CSP header
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
// In Next.js
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'; ..."
},
];
Cross-Site Request Forgery (CSRF) Protection
// CSRF token implementation
import { randomBytes } from 'crypto';
// Generate CSRF token
function generateCSRFToken(): string {
return randomBytes(32).toString('hex');
}
// Store in session
req.session.csrfToken = generateCSRFToken();
// Verify in requests
function verifyCSRFToken(req: Request, token: string): boolean {
return req.session.csrfToken === token;
}
// In forms
<form method="POST">
<input type="hidden" name="csrf_token" value="{csrfToken}" />
<!-- form fields -->
</form>
// Verify before processing
if (!verifyCSRFToken(req, req.body.csrf_token)) {
return Response.json({ error: 'Invalid CSRF token' }, { status: 403 });
}
Secure Headers
// Essential security headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
// Next.js implementation
// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
];
Rate Limiting
// Rate limiting middleware
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
// Different limits for different endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 login attempts per 15 minutes
skipSuccessfulRequests: true,
});
app.use('/api/auth/login', authLimiter);
app.use('/api/', limiter);
Secure Password Storage
import bcrypt from 'bcrypt';
// Hash password
async function hashPassword(password: string): Promise {
const saltRounds = 12; // Higher is more secure but slower
return await bcrypt.hash(password, saltRounds);
}
// Verify password
async function verifyPassword(
password: string,
hash: string
): Promise {
return await bcrypt.compare(password, hash);
}
// Password requirements
function validatePassword(password: string): boolean {
return (
password.length >= 12 &&
/[a-z]/.test(password) &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password) &&
/[^a-zA-Z0-9]/.test(password)
);
}
Secrets Management
- Never commit secrets to version control
- Use environment variables for configuration
- Use secret management services (AWS Secrets Manager, HashiCorp Vault)
- Rotate secrets regularly
- Use different secrets for different environments
Dependency Security
// Regularly audit dependencies
npm audit
npm audit fix
// Use Dependabot or Snyk for automated updates
// .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
// Lock file security
# Use package-lock.json or yarn.lock
# Verify checksums for critical packages
Security Testing
- Static Analysis: ESLint security plugins, SonarQube
- Dynamic Analysis: OWASP ZAP, Burp Suite
- Dependency Scanning: npm audit, Snyk, Dependabot
- Penetration Testing: Regular security audits
- Code Reviews: Security-focused reviews
Real-World Security Checklist
- β HTTPS enforced with HSTS
- β Strong authentication and session management
- β Input validation on all user inputs
- β Output encoding to prevent XSS
- β Parameterized queries to prevent SQL injection
- β CSRF protection on state-changing operations
- β Security headers configured
- β Rate limiting implemented
- β Secrets stored securely
- β Dependencies regularly updated
- β Security logging and monitoring
- β Regular security audits
Conclusion
Web application security requires a comprehensive approach. Implement defense in depth with multiple security layers, validate all inputs, use secure coding practices, and maintain security through regular updates and audits. Security is not a one-time task but an ongoing process that requires vigilance and continuous improvement.