All articles
Tutorials12 min readDecember 30, 2025
TutorialHands-OnCode ExamplesFixing Vulnerabilities

Tutorial: Fixing Your First Security Vulnerability (With Code Examples)

A hands-on guide to understanding and fixing common security vulnerabilities found by scanning tools.

Security Guide

From Finding to Fixed

You've run a security scan. Now you have findings. This tutorial walks through fixing the most common vulnerabilities with real code examples.

Understanding Vulnerability Reports

Every finding has key information:

Title:      SQL Injection in User Query
Severity:   Critical
File:       src/api/users.ts
Line:       23
CWE:        CWE-89
Confidence: High

Description: User input is directly interpolated into SQL query, allowing attackers to manipulate database queries.

Vulnerable Code: const users = await db.query(SELECT * FROM users WHERE name = '${name}')

What this tells you:

  • What: SQL injection
  • Where: src/api/users.ts, line 23
  • Why: User input in query string
  • How bad: Critical (exploitable)

Fix 1: SQL Injection

The Vulnerability

typescript
// src/api/users.ts - VULNERABLE

export async function GET(request: Request) { const { searchParams } = new URL(request.url) const name = searchParams.get('name')

// Line 23 - User input directly in query const users = await db.query( SELECT * FROM users WHERE name = '${name}' )

return Response.json(users) }

The attack:

URL: /api/users?name=' OR '1'='1
Query becomes: SELECT * FROM users WHERE name = '' OR '1'='1'
Result: Returns ALL users

The Fix

typescript
// src/api/users.ts - FIXED

export async function GET(request: Request) { const { searchParams } = new URL(request.url) const name = searchParams.get('name')

// Use parameterized query const users = await db.query( 'SELECT * FROM users WHERE name = $1', [name] )

return Response.json(users) }

Why this works: The database driver handles escaping. User input never becomes part of the query structure.

With Prisma/Drizzle (ORM)

typescript
// Even better - use ORM methods
const users = await prisma.user.findMany({
  where: { name: name }
})

Fix 2: XSS (Cross-Site Scripting)

The Vulnerability

tsx
// src/components/Comment.tsx - VULNERABLE

export function Comment({ content }: { content: string }) { return (

) }

The attack:

javascript
// User submits comment:
content = ""
// Script executes in other users' browsers

The Fix (Option 1: No HTML Needed)

tsx
// src/components/Comment.tsx - FIXED

export function Comment({ content }: { content: string }) { return (

{content}
) }

Why this works: React automatically escapes text content.

The Fix (Option 2: HTML Needed)

tsx
// src/components/Comment.tsx - FIXED with sanitization

import DOMPurify from 'dompurify'

export function Comment({ content }: { content: string }) { const sanitized = DOMPurify.sanitize(content, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'], ALLOWED_ATTR: ['href'] })

return (

) }

Fix 3: Hardcoded Secrets

The Vulnerability

typescript
// src/lib/stripe.ts - VULNERABLE

import Stripe from 'stripe'

const stripe = new Stripe('sk_live_abc123xyz789...', { apiVersion: '2023-10-16' })

export { stripe }

The Fix

Step 1: Create .env.local

bash
# .env.local (never commit this file)
STRIPE_SECRET_KEY=sk_live_abc123xyz789...

Step 2: Update .gitignore

bash
# .gitignore
.env.local
.env*.local

Step 3: Update code

typescript
// src/lib/stripe.ts - FIXED

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' })

export { stripe }

Step 4: Add to production environment

In Vercel/Railway/Render, add STRIPE_SECRET_KEY as an environment variable.

Step 5: Rotate the exposed key

The old key might be in git history. Generate a new key in Stripe dashboard.

Fix 4: Missing Authentication

The Vulnerability

typescript
// src/app/api/admin/users/route.ts - VULNERABLE

export async function GET() { const users = await prisma.user.findMany({ select: { id: true, email: true, role: true } })

return Response.json(users) }

The attack: Anyone can access /api/admin/users and see all users.

The Fix

typescript
// src/app/api/admin/users/route.ts - FIXED

import { getServerSession } from 'next-auth'

export async function GET() { // Check authentication const session = await getServerSession()

if (!session) { return Response.json( { error: 'Unauthorized' }, { status: 401 } ) }

// Check authorization if (session.user.role !== 'admin') { return Response.json( { error: 'Forbidden' }, { status: 403 } ) }

const users = await prisma.user.findMany({ select: { id: true, email: true, role: true } })

return Response.json(users) }

Fix 5: IDOR (Insecure Direct Object Reference)

The Vulnerability

typescript
// src/app/api/orders/[id]/route.ts - VULNERABLE

export async function GET( request: Request, { params }: { params: { id: string } } ) { const order = await prisma.order.findUnique({ where: { id: params.id } })

return Response.json(order) }

The attack: User A can access User B's orders by guessing IDs.

The Fix

typescript
// src/app/api/orders/[id]/route.ts - FIXED

import { getServerSession } from 'next-auth'

export async function GET( request: Request, { params }: { params: { id: string } } ) { const session = await getServerSession()

if (!session) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) }

// Include user ownership in query const order = await prisma.order.findFirst({ where: { id: params.id, userId: session.user.id // Only return if user owns it } })

if (!order) { return Response.json({ error: 'Not found' }, { status: 404 }) }

return Response.json(order) }

Verification Workflow

After each fix:

1. Test Locally

bash
# Run your app
npm run dev

# Test the fix manually curl http://localhost:3000/api/users?name="' OR '1'='1" # Should return empty or error, not all users

2. Re-scan

bash
# Push changes
git add .
git commit -m "Fix SQL injection in users API"
git push

# Re-run security scan # Finding should no longer appear

3. Confirm Resolution

Check that:

  • The specific finding is gone
  • No new findings introduced
  • App still works correctly

Common Fix Patterns Cheat Sheet

FindingFix Pattern
SQL InjectionUse parameterized queries or ORM
XSSUse textContent or sanitize HTML
Hardcoded SecretMove to environment variable
Missing AuthAdd authentication middleware
Missing AuthzAdd ownership check in query
Weak PasswordUse bcrypt with cost 12+
Missing Rate LimitAdd rate limiting middleware
SSRFValidate and allowlist URLs

When You're Stuck

Understanding the Issue

  1. Read the CWE reference (e.g., CWE-89 for SQL injection)
  2. Search "[vulnerability name] fix [your framework]"
  3. Ask AI: "How do I fix SQL injection in Next.js API routes?"

Getting Help

  1. Check documentation for your framework
  2. Search GitHub issues for similar findings
  3. Ask in community forums with specific code

The Bottom Line

Fixing vulnerabilities follows a pattern:

  1. Understand what the finding means
  2. Locate the vulnerable code
  3. Apply the secure pattern
  4. Test the fix
  5. Verify with re-scan
Most vulnerabilities have well-known fixes. The key is understanding the pattern.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free