Web application security is not optional—it's essential. A single vulnerability can lead to data breaches, financial losses, and irreparable damage to your reputation. This guide covers the most critical security practices every web developer must implement to protect their applications and users.
The OWASP Top 10
The Open Web Application Security Project (OWASP) maintains a list of the most critical web application security risks. Understanding and mitigating these is your first line of defense.
- Broken Access Control
- Cryptographic Failures
- Injection
- Insecure Design
- Security Misconfiguration
- Vulnerable and Outdated Components
- Identification and Authentication Failures
- Software and Data Integrity Failures
- Security Logging and Monitoring Failures
- Server-Side Request Forgery (SSRF)
Authentication and Authorization
Secure Password Handling
Never store passwords in plain text. Always use strong hashing algorithms like bcrypt, Argon2, or scrypt.
// Using bcrypt in Node.js
import bcrypt from 'bcrypt';
// Hash password on signup
async function hashPassword(password) {
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
}
// Verify password on login
async function verifyPassword(password, hashedPassword) {
const isMatch = await bcrypt.compare(password, hashedPassword);
return isMatch;
}
// DON'T DO THIS
const badHash = crypto.createHash('md5').update(password).digest('hex'); // INSECURE!
Password Requirements
- Minimum 12 characters
- Combination of uppercase, lowercase, numbers, and symbols
- Check against common password lists
- Implement password strength meter
- Enforce password expiration for sensitive systems
Multi-Factor Authentication (MFA)
Implement MFA for sensitive operations. Use authenticator apps (TOTP) rather than SMS when possible.
// Implementing TOTP with speakeasy
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
// Generate secret
const secret = speakeasy.generateSecret({
name: 'YourApp (user@example.com)',
length: 32,
});
// Generate QR code for user
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
// Verify token
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userEnteredToken,
window: 2, // Allow 2 time steps before/after
});
JSON Web Tokens (JWT) Best Practices
import jwt from 'jsonwebtoken';
// Generate token
function generateToken(userId) {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{
expiresIn: '15m', // Short-lived access token
issuer: 'your-app',
audience: 'your-app-users',
}
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users',
});
} catch (error) {
throw new Error('Invalid token');
}
}
// Use refresh tokens for long-term sessions
function generateRefreshToken(userId) {
return jwt.sign(
{ userId },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
}
JWT Security Tips
- Use strong, random secrets (minimum 256 bits)
- Set appropriate expiration times
- Store tokens securely (httpOnly cookies for web)
- Implement token rotation
- Maintain a token blacklist for logout
Cross-Site Scripting (XSS) Prevention
XSS attacks inject malicious scripts into your web pages. Prevent them by properly escaping user input and implementing Content Security Policy (CSP).
Input Sanitization
// Using DOMPurify in the browser
import DOMPurify from 'dompurify';
// Sanitize HTML input
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;
// React automatically escapes text content
function UserProfile({ username }) {
// This is safe - React escapes automatically
return {username};
// This is DANGEROUS - bypasses React's protection
return ; // DON'T DO THIS
}
Content Security Policy (CSP)
// Next.js - next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
`.replace(/\s{2,}/g, ' ').trim()
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}
];
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
Cross-Site Request Forgery (CSRF) Protection
CSRF attacks trick users into performing unwanted actions. Protect against them using CSRF tokens.
CSRF Token Implementation
// Express with csurf middleware
import csrf from 'csurf';
import cookieParser from 'cookie-parser';
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
// Send CSRF token to client
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Protect state-changing endpoints
app.post('/api/transfer-money', csrfProtection, (req, res) => {
// This will fail without valid CSRF token
// Process transfer...
});
// Frontend - Include CSRF token in requests
async function makeSecureRequest(url, data) {
// Get CSRF token
const response = await fetch('/api/csrf-token');
const { csrfToken } = await response.json();
// Include in request
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
});
}
SameSite Cookies
// Set secure cookie options
res.cookie('sessionId', sessionId, {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only
sameSite: 'strict', // Prevent CSRF
maxAge: 3600000, // 1 hour
path: '/',
domain: 'yourapp.com',
});
SQL Injection Prevention
Never concatenate user input into SQL queries. Always use parameterized queries or ORMs.
// VULNERABLE - Don't do this
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`; // SQL INJECTION!
// SAFE - Use parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
const result = await db.query(query, [userId]);
// SAFE - Using Prisma ORM
const user = await prisma.user.findUnique({
where: { id: userId }
});
// SAFE - Using PostgreSQL node-postgres
const result = await client.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
Secure Session Management
Session Configuration
import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';
const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 3600000, // 1 hour
sameSite: 'strict', // CSRF protection
},
name: 'sessionId', // Don't use default 'connect.sid'
}));
Session Best Practices
- Regenerate session ID after login
- Implement session timeout
- Clear session data on logout
- Use secure, random session IDs
- Store minimal data in sessions
// Regenerate session on login
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
if (user) {
// Regenerate session to prevent fixation
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Login failed' });
req.session.userId = user.id;
req.session.save(() => {
res.json({ success: true });
});
});
}
});
// Destroy session on logout
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Logout failed' });
res.clearCookie('sessionId');
res.json({ success: true });
});
});
Rate Limiting and DDoS Protection
import rateLimit from 'express-rate-limit';
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 login attempts per 15 minutes
skipSuccessfulRequests: true, // Don't count successful logins
});
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
HTTPS and TLS Configuration
Always Use HTTPS
Never transmit sensitive data over HTTP. Use HTTPS everywhere, enforce it with HSTS, and get free certificates from Let's Encrypt.
// Enforce HTTPS redirection
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
// Set HSTS header
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
Input Validation
Never trust user input. Validate everything on both client and server side.
// Using Zod for validation
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(12, 'Password must be at least 12 characters'),
age: z.number().min(18).max(120),
username: z.string().regex(/^[a-zA-Z0-9_]+$/, 'Invalid username'),
});
app.post('/api/register', async (req, res) => {
try {
// Validate input
const validatedData = userSchema.parse(req.body);
// Process registration...
res.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.errors });
}
res.status(500).json({ error: 'Server error' });
}
});
Security Logging and Monitoring
// Log security events
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'security.log' }),
],
});
// Log authentication attempts
function logAuthAttempt(userId, success, ip) {
logger.info('Authentication attempt', {
userId,
success,
ip,
timestamp: new Date().toISOString(),
});
}
// Log suspicious activity
function logSuspiciousActivity(type, details, ip) {
logger.warn('Suspicious activity detected', {
type,
details,
ip,
timestamp: new Date().toISOString(),
});
}
Dependency Security
# Regularly audit dependencies
npm audit
# Fix vulnerabilities automatically
npm audit fix
# Use tools like Snyk or GitHub Dependabot
# to monitor dependencies continuously
Security Checklist
- Keep dependencies updated
- Remove unused dependencies
- Review dependency permissions
- Use lock files (package-lock.json)
- Scan for known vulnerabilities
Environment Variables and Secrets
// Never commit secrets to version control
// Use .env files and .gitignore
// .env (NOT committed)
DATABASE_URL=postgresql://user:password@localhost:5432/db
JWT_SECRET=your-super-secret-key-min-256-bits
STRIPE_SECRET_KEY=sk_test_...
// .gitignore
.env
.env.local
.env.production
// Access in code
import dotenv from 'dotenv';
dotenv.config();
const dbUrl = process.env.DATABASE_URL;
Conclusion
Web security is an ongoing process, not a one-time task. Regularly review your security practices, stay updated on new vulnerabilities, conduct security audits, and never assume your application is "secure enough."
Security is not a product, but a process. - Bruce Schneier
Implement these best practices, educate your team, and make security a core part of your development workflow. Your users trust you with their data—honor that trust by prioritizing security at every level of your application.
Need a Security Audit?
Yonda Solutions offers comprehensive security audits and implementation services. We can review your application, identify vulnerabilities, and help implement robust security measures. Contact us today to secure your web application.