My Isekai Journey
16 min readUpdated 2026-02-16

Mastering Web Security: A Developer's Guide to Building Secure Applications

Learn how to protect your web applications against common vulnerabilities with practical examples and battle-tested patterns.

Cover image for Mastering Web Security: A Developer's Guide to Building Secure Applications

The wake-up call#

I learned about security the hard way. A few years ago, an app I built got hit by an SQL injection attack. Weeks of data. Compromised. The feeling of watching your database being dumped in real-time is something I never want to experience again.

That incident taught me: security isn't something you add at the end. It's foundational.

Let me share what I've learned about building secure web applications, so you never have to experience that panic.

The OWASP Top 10: Your security roadmap#

The Open Web Application Security Project (OWASP) maintains a list of the most critical web security risks. Let's tackle each one with practical Next.js examples.


1. Broken Access Control#

The problem: Users accessing resources they shouldn't.

Defense: Server-side authorization checks#

// lib/auth.ts
import { cookies } from "next/headers";
import { verify } from "jsonwebtoken";
 
export async function getCurrentUser() {
  const token = cookies().get("auth-token")?.value;
 
  if (!token) return null;
 
  try {
    const payload = verify(token, process.env.JWT_SECRET!) as {
      userId: string;
      role: string;
    };
 
    return payload;
  } catch {
    return null;
  }
}
 
export async function requireAuth() {
  const user = await getCurrentUser();
 
  if (!user) {
    throw new Error("Unauthorized");
  }
 
  return user;
}
 
export async function requireRole(role: string) {
  const user = await requireAuth();
 
  if (user.role !== role) {
    throw new Error("Forbidden");
  }
 
  return user;
}

Usage in API routes:

// app/api/admin/users/route.ts
import { requireRole } from "@/lib/auth";
import { NextResponse } from "next/server";
 
export async function GET() {
  try {
    // Only admins can access this endpoint
    await requireRole("admin");
 
    const users = await db.user.findMany({
      select: {
        id: true,
        email: true,
        createdAt: true,
        // Never expose passwords, even hashed
      },
    });
 
    return NextResponse.json(users);
  } catch (error) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
}

Resource-level access control:

// app/api/posts/[id]/route.ts
import { requireAuth } from "@/lib/auth";
import { NextResponse } from "next/server";
 
export async function PUT(
  request: Request,
  { params }: { params: { id: string } },
) {
  try {
    const user = await requireAuth();
 
    // Check if user owns the post
    const post = await db.post.findUnique({
      where: { id: params.id },
    });
 
    if (!post) {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
 
    if (post.authorId !== user.userId) {
      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
    }
 
    // User owns the post, allow update
    const body = await request.json();
    const updated = await db.post.update({
      where: { id: params.id },
      data: { title: body.title, content: body.content },
    });
 
    return NextResponse.json(updated);
  } catch (error) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
}

Access control flow diagram


2. Cryptographic Failures#

The problem: Exposing sensitive data through weak encryption or poor key management.

Defense: Proper encryption and hashing#

// lib/crypto.ts
import bcrypt from "bcrypt";
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
 
const SALT_ROUNDS = 12;
 
// Password hashing (one-way)
export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}
 
export async function verifyPassword(
  password: string,
  hash: string,
): Promise<boolean> {
  return bcrypt.compare(password, hash);
}
 
// Data encryption (two-way)
export function encrypt(text: string): string {
  const algorithm = "aes-256-gcm";
  const key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
  const iv = randomBytes(16);
 
  const cipher = createCipheriv(algorithm, key, iv);
 
  let encrypted = cipher.update(text, "utf8", "hex");
  encrypted += cipher.final("hex");
 
  const authTag = cipher.getAuthTag();
 
  // Return iv + authTag + encrypted data
  return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
}
 
export function decrypt(encryptedData: string): string {
  const algorithm = "aes-256-gcm";
  const key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
 
  const [ivHex, authTagHex, encrypted] = encryptedData.split(":");
  const iv = Buffer.from(ivHex, "hex");
  const authTag = Buffer.from(authTagHex, "hex");
 
  const decipher = createDecipheriv(algorithm, key, iv);
  decipher.setAuthTag(authTag);
 
  let decrypted = decipher.update(encrypted, "hex", "utf8");
  decrypted += decipher.final("utf8");
 
  return decrypted;
}

Secure user registration:

// app/api/auth/register/route.ts
import { hashPassword } from "@/lib/crypto";
import { NextResponse } from "next/server";
 
export async function POST(request: Request) {
  const { email, password } = await request.json();
 
  // Validate password strength
  if (password.length < 12) {
    return NextResponse.json(
      { error: "Password must be at least 12 characters" },
      { status: 400 },
    );
  }
 
  // Hash password (NEVER store plain text!)
  const hashedPassword = await hashPassword(password);
 
  const user = await db.user.create({
    data: {
      email,
      password: hashedPassword,
    },
  });
 
  // Don't return the password hash!
  return NextResponse.json({
    id: user.id,
    email: user.email,
  });
}

Storing sensitive data:

// app/api/integrations/route.ts
import { encrypt } from "@/lib/crypto";
import { requireAuth } from "@/lib/auth";
import { NextResponse } from "next/server";
 
export async function POST(request: Request) {
  const user = await requireAuth();
  const { apiKey } = await request.json();
 
  // Encrypt sensitive data before storing
  const encryptedApiKey = encrypt(apiKey);
 
  await db.integration.create({
    data: {
      userId: user.userId,
      apiKey: encryptedApiKey,
    },
  });
 
  return NextResponse.json({ success: true });
}

3. Injection Attacks#

The problem: Malicious data executed as code.

SQL Injection Defense#

// ❌ NEVER DO THIS: Direct string interpolation
async function getUser(email: string) {
  const query = `SELECT * FROM users WHERE email = '${email}'`;
  return db.execute(query);
}
// Attacker input: admin@example.com' OR '1'='1
 
// ✅ ALWAYS DO THIS: Parameterized queries
async function getUser(email: string) {
  return db.user.findUnique({
    where: { email }, // Prisma handles parameterization
  });
}
 
// ✅ Or with raw SQL:
async function getUserRaw(email: string) {
  return db.$queryRaw`
    SELECT * FROM users WHERE email = ${email}
  `; // Tagged template literal prevents injection
}

XSS (Cross-Site Scripting) Defense#

// ❌ DANGEROUS: dangerouslySetInnerHTML with user content
export function Comment({ content }: { content: string }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
// Attacker input: <script>steal_cookies()</script>
 
// ✅ SAFE: React escapes by default
export function Comment({ content }: { content: string }) {
  return <div>{content}</div>;
}
 
// ✅ If you need HTML, sanitize it first
import DOMPurify from "isomorphic-dompurify";
 
export function Comment({ content }: { content: string }) {
  const sanitized = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
    ALLOWED_ATTR: ["href"],
  });
 
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Content Security Policy (CSP):

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
 
  // Strict CSP to prevent XSS
  response.headers.set(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' https://trusted-cdn.com",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self' https://api.example.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
    ].join("; "),
  );
 
  return response;
}

Command Injection Defense#

// ❌ NEVER DO THIS: Shell commands with user input
import { exec } from "child_process";
 
async function resizeImage(filename: string) {
  exec(`convert ${filename} -resize 800x600 output.jpg`);
}
// Attacker input: "file.jpg; rm -rf /"
 
// ✅ DO THIS: Use libraries that don't spawn shells
import sharp from "sharp";
 
async function resizeImage(filename: string) {
  // Library handles the operation safely
  await sharp(filename).resize(800, 600).toFile("output.jpg");
}
 
// ✅ If you MUST use exec, validate strictly
import { exec } from "child_process";
import path from "path";
 
async function resizeImage(filename: string) {
  // Whitelist validation
  if (!/^[a-zA-Z0-9_-]+\.(jpg|png)$/.test(filename)) {
    throw new Error("Invalid filename");
  }
 
  // Use path.join to prevent directory traversal
  const safePath = path.join("/uploads", path.basename(filename));
 
  // Use array form to avoid shell
  exec(["convert", safePath, "-resize", "800x600", "output.jpg"]);
}

4. Insecure Design#

The problem: Fundamental security flaws in application architecture.

Defense: Security by design patterns#

Rate limiting:

// lib/rate-limit.ts
import { Redis } from "ioredis";
 
const redis = new Redis(process.env.REDIS_URL!);
 
export async function rateLimit(
  identifier: string,
  maxRequests: number,
  windowMs: number,
): Promise<{ limited: boolean; remaining: number }> {
  const key = `rate_limit:${identifier}`;
  const now = Date.now();
  const windowStart = now - windowMs;
 
  // Remove old entries
  await redis.zremrangebyscore(key, 0, windowStart);
 
  // Count requests in current window
  const count = await redis.zcard(key);
 
  if (count >= maxRequests) {
    return { limited: true, remaining: 0 };
  }
 
  // Add current request
  await redis.zadd(key, now, `${now}-${Math.random()}`);
  await redis.pexpire(key, windowMs);
 
  return { limited: false, remaining: maxRequests - count - 1 };
}
 
// Usage in API route
export async function POST(request: Request) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const { limited, remaining } = await rateLimit(ip, 10, 60000); // 10 req/min
 
  if (limited) {
    return NextResponse.json(
      { error: "Too many requests" },
      {
        status: 429,
        headers: {
          "X-RateLimit-Remaining": "0",
          "Retry-After": "60",
        },
      },
    );
  }
 
  // Process request...
  const response = NextResponse.json({ success: true });
  response.headers.set("X-RateLimit-Remaining", remaining.toString());
 
  return response;
}

CSRF Protection:

// lib/csrf.ts
import { cookies } from 'next/headers';
import { randomBytes } from 'crypto';
 
export function generateCSRFToken(): string {
  const token = randomBytes(32).toString('hex');
 
  cookies().set('csrf-token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 60 * 60 * 24, // 24 hours
  });
 
  return token;
}
 
export function verifyCSRFToken(token: string): boolean {
  const cookieToken = cookies().get('csrf-token')?.value;
 
  if (!cookieToken || !token) return false;
 
  // Constant-time comparison to prevent timing attacks
  return timingSafeEqual(
    Buffer.from(cookieToken),
    Buffer.from(token)
  );
}
 
// Usage in form
<form action="/api/submit" method="POST">
  <input type="hidden" name="csrf_token" value={csrfToken} />
  {/* other fields */}
</form>
 
// Verify in API route
export async function POST(request: Request) {
  const formData = await request.formData();
  const csrfToken = formData.get('csrf_token') as string;
 
  if (!verifyCSRFToken(csrfToken)) {
    return NextResponse.json(
      { error: 'Invalid CSRF token' },
      { status: 403 }
    );
  }
 
  // Process form...
}

Security layers diagram


5. Security Misconfiguration#

The problem: Insecure default configurations, unnecessary features, verbose error messages.

Defense: Secure defaults#

// next.config.ts
const securityHeaders = [
  {
    key: "X-DNS-Prefetch-Control",
    value: "on",
  },
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload",
  },
  {
    key: "X-Frame-Options",
    value: "SAMEORIGIN",
  },
  {
    key: "X-Content-Type-Options",
    value: "nosniff",
  },
  {
    key: "X-XSS-Protection",
    value: "1; mode=block",
  },
  {
    key: "Referrer-Policy",
    value: "strict-origin-when-cross-origin",
  },
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=()",
  },
];
 
export default {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: securityHeaders,
      },
    ];
  },
 
  // Don't expose Next.js version
  poweredByHeader: false,
};

Environment validation:

// lib/env.ts
import { z } from "zod";
 
const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  ENCRYPTION_KEY: z.string().length(64),
  NODE_ENV: z.enum(["development", "production", "test"]),
  REDIS_URL: z.string().url(),
});
 
// Validate at startup
export const env = envSchema.parse(process.env);
 
// Throws error if env vars are missing or invalid

Safe error messages:

// app/api/error-handler.ts
export function handleError(error: unknown) {
  console.error("Error:", error); // Log full error server-side
 
  if (process.env.NODE_ENV === "development") {
    // Show details in dev
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
 
  // Generic message in production (don't leak info!)
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}

6. Vulnerable and Outdated Components#

The problem: Using libraries with known vulnerabilities.

Defense: Dependency management#

# Check for vulnerabilities
npm audit
 
# Fix automatically when possible
npm audit fix
 
# Update all dependencies
npm update
 
# Use automated tools
npm install -g snyk
snyk test
snyk monitor

Dependabot configuration:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    versioning-strategy: increase
 
    # Auto-merge security patches
    pull-request-branch-name:
      separator: "-"
 
    # Group updates
    groups:
      dev-dependencies:
        dependency-type: "development"
      production-dependencies:
        dependency-type: "production"

Lock file integrity:

# Always commit lock files
git add package-lock.json
 
# Verify integrity before deployment
npm ci # Uses exact versions from lock file

7. Authentication Failures#

The problem: Weak authentication mechanisms.

Defense: Robust authentication#

// lib/auth-config.ts
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { verifyPassword } from "@/lib/crypto";
 
export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }
 
        const user = await db.user.findUnique({
          where: { email: credentials.email },
        });
 
        if (!user) {
          // Don't reveal if email exists
          return null;
        }
 
        const isValid = await verifyPassword(
          credentials.password,
          user.password,
        );
 
        if (!isValid) {
          // Log failed attempt
          await db.loginAttempt.create({
            data: {
              email: credentials.email,
              success: false,
              ip: "...", // Get from request
            },
          });
 
          // Check for brute force
          const recentAttempts = await db.loginAttempt.count({
            where: {
              email: credentials.email,
              success: false,
              createdAt: {
                gte: new Date(Date.now() - 15 * 60 * 1000), // Last 15 min
              },
            },
          });
 
          if (recentAttempts >= 5) {
            // Lock account temporarily
            await db.user.update({
              where: { id: user.id },
              data: { lockedUntil: new Date(Date.now() + 30 * 60 * 1000) },
            });
          }
 
          return null;
        }
 
        // Check if account is locked
        if (user.lockedUntil && user.lockedUntil > new Date()) {
          return null;
        }
 
        return {
          id: user.id,
          email: user.email,
          role: user.role,
        };
      },
    }),
  ],
 
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
 
  pages: {
    signIn: "/auth/signin",
  },
 
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
      }
      return token;
    },
 
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role;
      }
      return session;
    },
  },
};

Multi-factor authentication:

// lib/mfa.ts
import speakeasy from "speakeasy";
import QRCode from "qrcode";
 
export function generateMFASecret(email: string) {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${email})`,
    length: 32,
  });
 
  return {
    secret: secret.base32,
    qrCode: secret.otpauth_url,
  };
}
 
export async function generateMFAQRCode(otpauthUrl: string): Promise<string> {
  return QRCode.toDataURL(otpauthUrl);
}
 
export function verifyMFAToken(token: string, secret: string): boolean {
  return speakeasy.totp.verify({
    secret,
    encoding: "base32",
    token,
    window: 2, // Allow 2 time windows (±60s)
  });
}
 
// Enable MFA flow
export async function enableMFA(userId: string) {
  const user = await db.user.findUnique({ where: { id: userId } });
  if (!user) throw new Error("User not found");
 
  const { secret, qrCode } = generateMFASecret(user.email);
  const qrCodeImage = await generateMFAQRCode(qrCode);
 
  // Store secret temporarily
  await db.user.update({
    where: { id: userId },
    data: { mfaSecretTemp: secret },
  });
 
  return { qrCodeImage };
}
 
// Verify and activate MFA
export async function verifyAndActivateMFA(userId: string, token: string) {
  const user = await db.user.findUnique({ where: { id: userId } });
  if (!user?.mfaSecretTemp) throw new Error("MFA not initialized");
 
  const isValid = verifyMFAToken(token, user.mfaSecretTemp);
 
  if (!isValid) {
    throw new Error("Invalid token");
  }
 
  // Activate MFA
  await db.user.update({
    where: { id: userId },
    data: {
      mfaSecret: user.mfaSecretTemp,
      mfaSecretTemp: null,
      mfaEnabled: true,
    },
  });
 
  return true;
}

8. Data Integrity Failures#

The problem: Data tampering or unauthorized modifications.

Defense: Integrity verification#

// lib/integrity.ts
import { createHmac } from "crypto";
 
export function generateSignature(data: string, secret: string): string {
  return createHmac("sha256", secret).update(data).digest("hex");
}
 
export function verifySignature(
  data: string,
  signature: string,
  secret: string,
): boolean {
  const expected = generateSignature(data, secret);
 
  // Constant-time comparison
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
 
// Usage: Webhook verification
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-signature");
 
  if (
    !signature ||
    !verifySignature(body, signature, process.env.WEBHOOK_SECRET!)
  ) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }
 
  // Process webhook...
}

9. Logging and Monitoring Failures#

The problem: Not detecting or responding to security incidents.

Defense: Comprehensive logging#

// lib/security-logger.ts
import winston from "winston";
 
export const securityLogger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  defaultMeta: { service: "security" },
  transports: [
    new winston.transports.File({
      filename: "logs/security.log",
      level: "warn",
    }),
  ],
});
 
export function logSecurityEvent(event: {
  type:
    | "auth_failure"
    | "access_denied"
    | "suspicious_activity"
    | "data_breach_attempt";
  userId?: string;
  ip: string;
  details: Record<string, unknown>;
}) {
  securityLogger.warn("Security event", {
    ...event,
    timestamp: new Date().toISOString(),
  });
 
  // Alert on critical events
  if (event.type === "data_breach_attempt") {
    // Send alert to security team
    alertSecurityTeam(event);
  }
}
 
// Usage
export async function POST(request: Request) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
 
  try {
    // Attempt authentication...
  } catch (error) {
    logSecurityEvent({
      type: "auth_failure",
      ip,
      details: { error: String(error) },
    });
 
    throw error;
  }
}

10. Server-Side Request Forgery (SSRF)#

The problem: Attacker makes server send requests to internal resources.

Defense: Validate and sanitize URLs#

// lib/safe-fetch.ts
export async function safeFetch(url: string) {
  const parsed = new URL(url);
 
  // Block private IPs
  const blockedHosts = [
    "localhost",
    "127.0.0.1",
    "0.0.0.0",
    "169.254.169.254", // AWS metadata
    "::1",
  ];
 
  if (blockedHosts.includes(parsed.hostname)) {
    throw new Error("Access to this host is not allowed");
  }
 
  // Block private IP ranges
  const ipRegex = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
  if (ipRegex.test(parsed.hostname)) {
    throw new Error("Access to private IPs is not allowed");
  }
 
  // Only allow HTTP/HTTPS
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("Only HTTP/HTTPS protocols are allowed");
  }
 
  // Fetch with timeout
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
 
  try {
    const response = await fetch(url, {
      signal: controller.signal,
      redirect: "manual", // Don't follow redirects automatically
    });
 
    return response;
  } finally {
    clearTimeout(timeout);
  }
}
 
// Usage
export async function POST(request: Request) {
  const { url } = await request.json();
 
  try {
    const response = await safeFetch(url);
    const data = await response.text();
 
    return NextResponse.json({ data });
  } catch (error) {
    return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
  }
}

Security checklist#

// lib/security-checklist.ts
export const securityChecklist = {
  authentication: [
    "✓ Passwords are hashed with bcrypt (12+ rounds)",
    "✓ JWT secrets are 256+ bits",
    "✓ Session tokens expire",
    "✓ Account lockout after failed attempts",
    "✓ MFA available for sensitive actions",
  ],
 
  authorization: [
    "✓ Server-side auth checks on every request",
    "✓ Resource-level access control",
    "✓ Role-based access control",
    "✓ Principle of least privilege",
  ],
 
  dataProtection: [
    "✓ Sensitive data encrypted at rest",
    "✓ HTTPS everywhere",
    "✓ No secrets in code/logs",
    "✓ Regular backups",
    "✓ Secure key management",
  ],
 
  inputValidation: [
    "✓ All inputs validated server-side",
    "✓ Using parameterized queries",
    "✓ Output encoding",
    "✓ CSP headers configured",
    "✓ CSRF protection enabled",
  ],
 
  infrastructure: [
    "✓ Dependencies up to date",
    "✓ Security headers configured",
    "✓ Rate limiting enabled",
    "✓ Error messages don't leak info",
    "✓ Audit logging enabled",
  ],
};

Security checklist visualization


Testing security#

# Install security testing tools
npm install --save-dev @typescript-eslint/eslint-plugin-security
 
# Use helmet for security headers
npm install helmet
 
# Run security audit
npm audit
npm audit fix
 
# Use OWASP ZAP for penetration testing
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://yoursite.com

The reality of security#

Security is not a feature you ship once. It's an ongoing practice:

  1. Stay informed: Follow security newsletters, CVE databases
  2. Regular audits: Review code and dependencies quarterly
  3. Incident response plan: Know what to do when breached
  4. Security training: Educate your team
  5. Bug bounty program: Let ethical hackers find issues

Your 30-day security sprint#

Week 1: Authentication & Authorization

  • Implement proper password hashing
  • Add auth checks to all API routes
  • Enable account lockout

Week 2: Input Validation & Injection Prevention

  • Use parameterized queries everywhere
  • Implement CSP headers
  • Sanitize user input

Week 3: Infrastructure Hardening

  • Configure security headers
  • Set up rate limiting
  • Validate environment variables

Week 4: Monitoring & Testing

  • Add security logging
  • Run security audit
  • Create incident response plan

Resources for deeper learning#


Security is a journey#

You can't make your app 100% secure. But you can make it 100x harder to attack.

Start with the basics: authentication, input validation, proper encryption. Build on that foundation. Monitor continuously. Respond quickly when issues arise.

Your users trust you with their data. Honor that trust.

What security improvement will you tackle first?