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: HighDescription:
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
// src/api/users.ts - VULNERABLEexport 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 usersThe Fix
// src/api/users.ts - FIXEDexport 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)
// Even better - use ORM methods
const users = await prisma.user.findMany({
where: { name: name }
})Fix 2: XSS (Cross-Site Scripting)
The Vulnerability
// src/components/Comment.tsx - VULNERABLEexport function Comment({ content }: { content: string }) {
return (
)
}The attack:
// User submits comment:
content = ""
// Script executes in other users' browsersThe Fix (Option 1: No HTML Needed)
// src/components/Comment.tsx - FIXEDexport function Comment({ content }: { content: string }) {
return (
{content}
)
}Why this works: React automatically escapes text content.
The Fix (Option 2: HTML Needed)
// src/components/Comment.tsx - FIXED with sanitizationimport 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
// src/lib/stripe.ts - VULNERABLEimport Stripe from 'stripe'
const stripe = new Stripe('sk_live_abc123xyz789...', {
apiVersion: '2023-10-16'
})
export { stripe }
The Fix
Step 1: Create .env.local
# .env.local (never commit this file)
STRIPE_SECRET_KEY=sk_live_abc123xyz789...Step 2: Update .gitignore
# .gitignore
.env.local
.env*.localStep 3: Update code
// src/lib/stripe.ts - FIXEDimport 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
// src/app/api/admin/users/route.ts - VULNERABLEexport 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
// src/app/api/admin/users/route.ts - FIXEDimport { 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
// src/app/api/orders/[id]/route.ts - VULNERABLEexport 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
// src/app/api/orders/[id]/route.ts - FIXEDimport { 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
# 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
# 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
| Finding | Fix Pattern |
|---|
| SQL Injection | Use parameterized queries or ORM |
|---|
| XSS | Use textContent or sanitize HTML |
|---|
| Hardcoded Secret | Move to environment variable |
|---|
| Missing Auth | Add authentication middleware |
|---|
| Missing Authz | Add ownership check in query |
|---|
| Weak Password | Use bcrypt with cost 12+ |
|---|
| Missing Rate Limit | Add rate limiting middleware |
|---|
| SSRF | Validate and allowlist URLs |
|---|
When You're Stuck
Understanding the Issue
- Read the CWE reference (e.g., CWE-89 for SQL injection)
- Search "[vulnerability name] fix [your framework]"
- Ask AI: "How do I fix SQL injection in Next.js API routes?"
Getting Help
- Check documentation for your framework
- Search GitHub issues for similar findings
- Ask in community forums with specific code
The Bottom Line
Fixing vulnerabilities follows a pattern:
- Understand what the finding means
- Locate the vulnerable code
- Apply the secure pattern
- Test the fix
- Verify with re-scan