Introduction
Security breaches cost companies an average of $4.45 million per incident in 2024, yet many developers still treat security as an afterthought. The "secure-by-design" approach flips this paradigm by baking security into every layer of your application from day one.
The OWASP Top 10 provides a consensus-driven framework for the most critical web application security risks. In this comprehensive guide, we'll walk through implementing each of these controls in a modern JavaScript stack using React, Next.js, and Node.js with Express.
Whether you're building your first production app or hardening an existing system, this guide will give you practical, copy-paste-ready solutions that you can implement today.
Understanding the OWASP Top 10 (2021 Edition)
Before diving into implementation, let's quickly overview the current OWASP Top 10:
- Broken Access Control - Users can act outside their intended permissions
- Cryptographic Failures - Inadequate protection of sensitive data
- Injection - Untrusted data sent to interpreters
- Insecure Design - Missing or ineffective security controls
- Security Misconfiguration - Improper security settings
- Vulnerable and Outdated Components - Using components with known vulnerabilities
- Identification and Authentication Failures - Weak authentication mechanisms
- Software and Data Integrity Failures - Unverified code and data
- Security Logging and Monitoring Failures - Insufficient logging
- Server-Side Request Forgery (SSRF) - Fetching remote resources without validation
Now let's implement defenses for each.
1. Broken Access Control
The Problem
Access control enforces policies so users can't act outside their permissions. Without proper controls, attackers can access unauthorized functionality or data.
Implementation: Role-Based Access Control (RBAC)
Backend Middleware (Node.js/Express)
// middleware/auth.js
import jwt from 'jsonwebtoken';
export const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
export const authorize = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
};
// Usage in routes
app.delete('/api/users/:id',
authenticate,
authorize('admin'),
deleteUser
);
Frontend Implementation (React)
// hooks/useAuth.js
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Verify token on mount
const token = localStorage.getItem('token');
if (token) {
verifyToken(token).then(setUser).finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const hasPermission = (requiredRole) => {
if (!user) return false;
const roleHierarchy = { user: 1, moderator: 2, admin: 3 };
return roleHierarchy[user.role] >= roleHierarchy[requiredRole];
};
return (
<AuthContext.Provider value={{ user, hasPermission, loading }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
// Protected Component
const AdminPanel = () => {
const { hasPermission } = useAuth();
if (!hasPermission('admin')) {
return <div>Access Denied</div>;
}
return <div>Admin Content</div>;
};
Common Mistakes
- Client-side only checks - Always validate on the server
- Predictable IDs - Use UUIDs instead of sequential integers
- Missing ownership checks - Verify the user owns the resource they're accessing
2. Cryptographic Failures
The Problem
Sensitive data like passwords, credit cards, and personal information must be protected in transit and at rest.
Implementation: Encryption Best Practices
Password Hashing with bcrypt
// utils/password.js
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export const hashPassword = async (password) => {
// Never store plain text passwords
return await bcrypt.hash(password, SALT_ROUNDS);
};
export const verifyPassword = async (password, hash) => {
return await bcrypt.compare(password, hash);
};
// User registration
app.post('/api/register', async (req, res) => {
const { email, password } = req.body;
// Validate password strength
if (password.length < 12) {
return res.status(400).json({
error: 'Password must be at least 12 characters'
});
}
const hashedPassword = await hashPassword(password);
await db.users.create({
email,
password: hashedPassword
});
res.status(201).json({ message: 'User created' });
});
Encrypting Sensitive Data at Rest
// utils/encryption.js
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
export const encrypt = (text) => {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
};
export const decrypt = (encryptedData) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
KEY,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
// Store sensitive data
const sensitiveData = encrypt(req.body.creditCard);
await db.payments.create(sensitiveData);
HTTPS Enforcement (Next.js)
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
// Force HTTPS in production
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
return NextResponse.redirect(
`https://${request.headers.get('host')}${request.nextUrl.pathname}`,
301
);
}
// Set security headers
const response = NextResponse.next();
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return response;
}
3. Injection Attacks
The Problem
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL, NoSQL, OS, and LDAP injection are common variants.
Implementation: Input Validation & Parameterized Queries
SQL Injection Prevention
// ❌ VULNERABLE CODE - Never do this!
app.get('/api/users', async (req, res) => {
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
const result = await db.query(query); // DANGEROUS!
});
// ✅ SECURE CODE - Use parameterized queries
app.get('/api/users', async (req, res) => {
const userId = req.query.id;
// With PostgreSQL (pg library)
const result = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
res.json(result.rows);
});
NoSQL Injection Prevention (MongoDB)
import { body, validationResult } from 'express-validator';
// ❌ VULNERABLE
app.post('/api/login', async (req, res) => {
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
// Attack: { "username": { "$gt": "" }, "password": { "$gt": "" } }
});
// ✅ SECURE - Validate and sanitize
app.post('/api/login', [
body('username').isString().trim().escape(),
body('password').isString()
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, password } = req.body;
const user = await User.findOne({
username: String(username)
});
if (!user || !(await verifyPassword(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
res.json({ token: generateToken(user) });
});
Command Injection Prevention
// ❌ VULNERABLE - User input in shell commands
const { exec } = require('child_process');
app.get('/api/ping', (req, res) => {
exec(`ping -c 4 ${req.query.host}`, (error, stdout) => {
res.send(stdout);
});
// Attack: ?host=google.com;rm -rf /
});
// ✅ SECURE - Use libraries instead of shell commands
import ping from 'ping';
app.get('/api/ping', async (req, res) => {
const host = req.query.host;
// Validate input
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).json({ error: 'Invalid host' });
}
const result = await ping.promise.probe(host);
res.json(result);
});
XSS Prevention in React
// React automatically escapes output, but be careful with dangerouslySetInnerHTML
// ❌ VULNERABLE
const UserComment = ({ comment }) => {
return <div dangerouslySetInnerHTML={{ __html: comment }} />;
};
// ✅ SECURE - Sanitize HTML
import DOMPurify from 'isomorphic-dompurify';
const UserComment = ({ comment }) => {
const sanitized = DOMPurify.sanitize(comment, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
};
4. Insecure Design
The Problem
Insecure design represents missing or ineffective security controls at the design phase, not implementation bugs.
Implementation: Security Design Patterns
Rate Limiting to Prevent Brute Force
import rateLimit from 'express-rate-limit';
// Global rate limiter
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 login attempts per 15 minutes
skipSuccessfulRequests: true
});
app.use('/api/', globalLimiter);
app.post('/api/login', authLimiter, loginHandler);
Implementing Account Lockout
// models/User.js
const userSchema = new Schema({
email: String,
password: String,
loginAttempts: { type: Number, default: 0 },
lockUntil: Date
});
userSchema.methods.isLocked = function() {
return this.lockUntil && this.lockUntil > Date.now();
};
userSchema.methods.incrementLoginAttempts = async function() {
// Reset attempts if lock has expired
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.updateOne({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
});
}
const updates = { $inc: { loginAttempts: 1 } };
// Lock account after 5 failed attempts
if (this.loginAttempts + 1 >= 5) {
updates.$set = {
lockUntil: Date.now() + 2 * 60 * 60 * 1000 // 2 hours
};
}
return this.updateOne(updates);
};
// Login handler
app.post('/api/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
if (user.isLocked()) {
return res.status(423).json({
error: 'Account locked. Try again later.'
});
}
const isValid = await verifyPassword(req.body.password, user.password);
if (!isValid) {
await user.incrementLoginAttempts();
return res.status(401).json({ error: 'Invalid credentials' });
}
// Reset attempts on successful login
await user.updateOne({
$set: { loginAttempts: 0 },
$unset: { lockUntil: 1 }
});
res.json({ token: generateToken(user) });
});
5. Security Misconfiguration
The Problem
Security misconfiguration includes improper HTTP headers, verbose error messages, default credentials, and unnecessary features enabled.
Implementation: Secure Configuration
Security Headers with Helmet
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
Next.js Security Headers
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}
]
}
];
}
};
Environment Variables Security
// .env.example (commit this)
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
ENCRYPTION_KEY=
// .env (never commit this!)
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@localhost/db
JWT_SECRET=your-256-bit-secret
ENCRYPTION_KEY=your-hex-encryption-key
// config/env.js - Validate required vars
const requiredEnvVars = [
'DATABASE_URL',
'JWT_SECRET',
'ENCRYPTION_KEY'
];
requiredEnvVars.forEach((varName) => {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
});
// Never expose secrets in error messages
app.use((err, req, res, next) => {
console.error(err.stack); // Log full error server-side
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
});
6. Vulnerable and Outdated Components
The Problem
Using components with known vulnerabilities can expose your entire application to attack.
Implementation: Dependency Management
Automated Dependency Scanning
// package.json
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"outdated": "npm outdated",
"update:check": "npx npm-check-updates"
},
"devDependencies": {
"npm-check-updates": "^16.14.0"
}
}
GitHub Actions for Security Scanning
# .github/workflows/security.yml
name: Security Audit
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run security audit
run: npm audit --audit-level=moderate
- name: Check for outdated packages
run: npm outdated || true
Dependency Version Pinning
// package.json - Use exact versions in production
{
"dependencies": {
"express": "4.18.2", // Exact version
"mongoose": "^7.0.0" // OK for minor updates
},
"devDependencies": {
"jest": "~29.5.0" // OK for patch updates
}
}
// Use package-lock.json or yarn.lock
// Commit these files to ensure consistent installs
7. Identification and Authentication Failures
The Problem
Authentication-related vulnerabilities allow attackers to assume other users' identities.
Implementation: Robust Authentication
Implementing JWT with Refresh Tokens
// utils/tokens.js
import jwt from 'jsonwebtoken';
export const generateAccessToken = (user) => {
return jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short-lived
);
};
export const generateRefreshToken = (user) => {
return jwt.sign(
{ id: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
};
// Login endpoint
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await verifyPassword(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Store refresh token (hashed)
await db.refreshTokens.create({
userId: user.id,
token: await hashPassword(refreshToken),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json({
accessToken,
refreshToken
});
});
// Refresh endpoint
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// Verify token exists in database
const storedTokens = await db.refreshTokens.find({
userId: decoded.id
});
const isValid = await Promise.any(
storedTokens.map(t => verifyPassword(refreshToken, t.token))
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const user = await User.findById(decoded.id);
const newAccessToken = generateAccessToken(user);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Multi-Factor Authentication (MFA)
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
// Enable MFA
app.post('/api/mfa/setup', authenticate, async (req, res) => {
const secret = speakeasy.generateSecret({
name: `YourApp (${req.user.email})`
});
// Store secret (encrypted!)
await User.findByIdAndUpdate(req.user.id, {
mfaSecret: encrypt(secret.base32)
});
// Generate QR code
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
res.json({ qrCode, secret: secret.base32 });
});
// Verify MFA
app.post('/api/mfa/verify', authenticate, async (req, res) => {
const { token } = req.body;
const user = await User.findById(req.user.id);
const secret = decrypt(user.mfaSecret);
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2 // Allow 2 time steps of tolerance
});
if (verified) {
await User.findByIdAndUpdate(user.id, { mfaEnabled: true });
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid MFA code' });
}
});
8. Software and Data Integrity Failures
The Problem
Code and infrastructure that don't protect against integrity violations can lead to unauthorized access or malicious code execution.
Implementation: Integrity Checks
Subresource Integrity (SRI) for CDN Assets
// Next.js _document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<link
rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossOrigin="anonymous"
/>
<script
src="https://cdn.example.com/script.js"
integrity="sha384-abc123..."
crossOrigin="anonymous"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Digital Signatures for API Responses
import crypto from 'crypto';
// Generate signature
const signData = (data, secret) => {
return crypto
.createHmac('sha256', secret)
.update(JSON.stringify(data))
.digest('hex');
};
// API endpoint
app.get('/api/critical-data', authenticate, async (req, res) => {
const data = await getCriticalData();
const signature = signData(data, process.env.SIGNING_SECRET);
res.json({
data,
signature,
timestamp: Date.now()
});
});
// Client-side verification
const verifyResponse = (response) => {
const { data, signature, timestamp } = response;
// Check timestamp (prevent replay attacks)
if (Date.now() - timestamp > 60000) { // 1 minute
throw new Error('Response expired');
}
const expectedSignature = signData(data, process.env.SIGNING_SECRET);
if (signature !== expectedSignature) {
throw new Error('Invalid signature');
}
return data;
};
CI/CD Pipeline Security
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
security-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run dependency audit
run: npm audit --audit-level=high
- name: Run SAST scan
uses: github/codeql-action/analyze@v2
- name: Verify package integrity
run: npm ci --prefer-offline
deploy:
needs: security-checks
runs-on: ubuntu-latest
steps:
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
# Deployment steps
9. Security Logging and Monitoring Failures
The Problem
Without proper logging and monitoring, breaches can go undetected for months.
Implementation: Comprehensive Logging
Structured Logging with Winston
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'api' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Security event logging
const logSecurityEvent = (event, details) => {
logger.warn('Security Event', {
type: event,
...details,
timestamp: new Date().toISOString()
});
};
// Usage in middleware
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await verifyPassword(password, user.password))) {
logSecurityEvent('failed_login', {
email,
ip: req.ip,
userAgent: req.get('user-agent')
});
return res.status(401).json({ error: 'Invalid credentials' });
}
logSecurityEvent('successful_login', {
userId: user.id,
email: user.email,
ip: req.ip
});
res.json({ token: generateToken(user) });
});
Real-time Alerting
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.ALERT_EMAIL,
pass: process.env.ALERT_PASSWORD
}
});
const sendSecurityAlert = async (subject, details) => {
await transporter.sendMail({
from: process.env.ALERT_EMAIL,
to: process.env.SECURITY_TEAM_EMAIL,
subject: `🚨 Security Alert: ${subject}`,
html: `
<h2>Security Alert</h2>
<pre>${JSON.stringify(details, null, 2)}</pre>
<p>Time: ${new Date().toISOString()}</p>
`
});
};
// Detect suspicious activity
const suspiciousActivityDetector = async (req, res, next) => {
const userId = req.user?.id;
const key = `activity:${userId || req.ip}`;
const count = await redis.incr(key);
await redis.expire(key, 60); // 1 minute window
if (count > 100) { // More than 100 requests per minute
await sendSecurityAlert('Suspicious Activity Detected', {
userId,
ip: req.ip,
requestCount: count,
endpoint: req.path
});
return res.status(429).json({ error: 'Too many requests' });
}
next();
};
10. Server-Side Request Forgery (SSRF)
The Problem
SSRF flaws occur when a web application fetches a remote resource without validating the user-supplied URL.
Implementation: URL Validation
import { URL } from 'url';
import axios from 'axios';
// Whitelist of allowed domains
const ALLOWED_DOMAINS = [
'api.example.com',
'cdn.example.com'
];
// Blacklist of internal IP ranges
const isInternalIP = (hostname) => {
const internalRanges = [
/^127\./, // Loopback
/^10\./, // Private network
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private network
/^192\.168\./, // Private network
/^169\.254\./, // Link-local
/^localhost$/i,
/^0\.0\.0\.0$/
];
return internalRanges.some(range => range.test(hostname));
};
const validateURL = (urlString) => {
try {
const url = new URL(urlString);
// Only allow HTTP(S)
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
// Check against whitelist
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
// Block internal IPs
if (isInternalIP(url.hostname)) {
throw new Error('Internal IPs not allowed');
}
return url.href;
} catch (error) {
throw new Error('Invalid URL');
}
};
// Fetch external resource safely
app.post('/api/fetch-resource', authenticate, async (req, res) => {
const { url } = req.body;
try {
const validatedURL = validateURL(url);
const response = await axios.get(validatedURL, {
timeout: 5000,
maxRedirects: 0, // Don't follow redirects
maxContentLength: 1024 * 1024 // 1MB limit
});
res.json(response.data);
} catch (error) {
logSecurityEvent('ssrf_attempt', {
userId: req.user.id,
requestedURL: url,
error: error.message
});
res.status(400).json({ error: 'Invalid request' });
}
});
🚀 Pro Tips
1. Security Headers Testing
Use securityheaders.com to scan your deployed application and get an A+ rating.
2. Automate Security in Your Workflow
# Add to pre-commit hook
npm audit --audit-level=moderate && npm run lint:security
3. Use TypeScript for Type Safety
TypeScript catches entire classes of bugs at compile time:
// Prevents passing wrong types to sensitive functions
interface User {
id: string;
email: string;
role: 'user' | 'admin' | 'moderator';
}
const authorize = (user: User, requiredRole: User['role']) => {
// TypeScript ensures role is one of the allowed values
return user.role === requiredRole;
};
4. Implement Security.txt
Create a public/.well-known/security.txt file:
Contact: security@yourapp.com
Expires: 2027-01-01T00:00:00.000Z
Preferred-Languages: en
Canonical: https://yourapp.com/.well-known/security.txt
5. Regular Penetration Testing
Schedule quarterly penetration tests and use tools like:
- OWASP ZAP (free, open-source)
- Burp Suite (freemium)
- npm audit / Snyk (dependency scanning)
Common Mistakes to Avoid
1. Rolling Your Own Crypto
Never implement your own encryption algorithms. Use battle-tested libraries like bcrypt, crypto, and jsonwebtoken.
2. Trusting Client-Side Validation
Always validate on the server. Client-side validation is UX, not security.
3. Logging Sensitive Data
// ❌ DON'T
logger.info('User login', { email, password });
// ✅ DO
logger.info('User login', { email, ip: req.ip });
4. Ignoring Dependency Updates
Set up Dependabot or Renovate to automatically create PRs for dependency updates.
5. Over-Permissive CORS
// ❌ DON'T
app.use(cors({ origin: '*' }));
// ✅ DO
app.use(cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true
}));
📌 Key Takeaways
-
Security is a journey, not a destination - Continuously update and improve your security posture
-
Defense in depth - Implement multiple layers of security controls
-
Validate everything - Never trust user input, always validate and sanitize
-
Use established libraries - Don't reinvent the wheel for security-critical functions
-
Log security events - You can't fix what you don't know about
-
Keep dependencies updated - Automate security scans and updates
-
Fail securely - Error messages should be informative to developers but vague to users
-
Encrypt sensitive data - Both in transit (HTTPS) and at rest (encryption at database level)
-
Implement proper authentication - Use JWT with refresh tokens and consider MFA
-
Test your security - Regular audits, penetration tests, and code reviews are essential
Conclusion
Building secure web applications requires a proactive mindset and systematic implementation of security controls at every layer. The OWASP Top 10 provides an excellent framework, but security is an ongoing process that requires continuous learning and adaptation.
By implementing the patterns and practices outlined in this guide, you'll have a solid foundation for building secure applications in the modern JavaScript ecosystem. Remember that security is not a checklist to complete but a continuous practice to refine.
Start small—pick one area from this guide and implement it in your current project today. Then gradually expand your security coverage until it becomes second nature.
Stay secure, and happy coding! 🔒
Additional Resources: