- Learn
- Advanced Topics
- Security Best Practices
Advanced25 min1 prerequisite
Ensure AI-generated code follows security best practices to protect against common vulnerabilities.
Security Best Practices
Learn to identify and fix security vulnerabilities in AI-generated code, protecting your applications from common attacks.
Why AI Code Needs Security Review
AI Security Tendencies
Terminal
AI-generated code often:
├── Prioritizes functionality over security
├── Uses outdated security patterns
├── Misses edge cases in validation
├── Hardcodes sensitive values
├── Trusts user input implicitly
└── Lacks proper error handling
Real-World Impact
Terminal
Common AI-introduced vulnerabilities:
- SQL Injection (unsanitized queries)
- XSS (unescaped user content)
- Authentication bypass (missing checks)
- Information disclosure (verbose errors)
- CSRF (missing tokens)
Input Validation
Server-Side Validation
Terminal
// ❌ AI often generates client-side only validation
function CreateUser({ onSubmit }) {
const [email, setEmail] = useState('')
const handleSubmit = () => {
if (email.includes('@')) { // Client-side only!
onSubmit(email)
}
}
}
// ✓ Always validate on server
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150),
})
export async function POST(request: Request) {
const body = await request.json()
const result = userSchema.safeParse(body)
if (!result.success) {
return Response.json(
{ error: 'Invalid input', details: result.error.issues },
{ status: 400 }
)
}
// result.data is now typed and validated
const { email, name, age } = result.data
// ...
}
Sanitizing Output
Terminal
// ❌ Directly rendering user content
function Comment({ comment }) {
return <div dangerouslySetInnerHTML={{ __html: comment.body }} />
}
// ✓ Sanitize HTML or use text content
import DOMPurify from 'dompurify'
function Comment({ comment }) {
// Option 1: Sanitize if HTML is needed
const sanitized = DOMPurify.sanitize(comment.body)
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />
// Option 2: Plain text (preferred)
return <div>{comment.body}</div>
}
SQL Injection Prevention
Use Parameterized Queries
Terminal
// ❌ String interpolation (SQL Injection vulnerable)
const query = `SELECT * FROM users WHERE id = '${userId}'`
// ✓ Parameterized query (Supabase)
const { data } = await supabase
.from('users')
.select('*')
.eq('id', userId)
// ✓ Parameterized query (Prisma)
const user = await prisma.user.findUnique({
where: { id: userId }
})
// ✓ Parameterized query (raw SQL)
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE id = ${userId}
`
Validate IDs and Parameters
Terminal
// ✓ Validate UUID format before using
import { z } from 'zod'
const uuidSchema = z.string().uuid()
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const result = uuidSchema.safeParse(id)
if (!result.success) {
return Response.json({ error: 'Invalid ID' }, { status: 400 })
}
const user = await getUser(result.data)
// ...
}
Authentication & Authorization
Verify Authentication on Every Request
Terminal
// ❌ Missing auth check
export async function GET(request: Request) {
const orders = await db.orders.findMany()
return Response.json(orders)
}
// ✓ Always verify auth
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const orders = await db.orders.findMany({
where: { userId: user.id } // Also filter by user!
})
return Response.json(orders)
}
Authorization Checks
Terminal
// ❌ Only checking if logged in
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await getUser()
if (!user) return unauthorized()
await db.posts.delete({ where: { id } }) // Anyone can delete!
}
// ✓ Verify ownership or role
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await getUser()
if (!user) return unauthorized()
const post = await db.posts.findUnique({ where: { id } })
// Check ownership
if (post.authorId !== user.id) {
// Or check admin role
if (user.role !== 'admin') {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
}
await db.posts.delete({ where: { id } })
}
Sensitive Data Handling
Environment Variables
Terminal
// ❌ Hardcoded secrets
const STRIPE_KEY = 'sk_live_abc123...'
const DATABASE_URL = 'postgresql://user:password@host/db'
// ✓ Environment variables
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY!
const DATABASE_URL = process.env.DATABASE_URL!
Client vs Server Variables
Terminal
# .env.local
# Server-only (never exposed to browser)
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://...
JWT_SECRET=...
# Public (exposed to browser)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_API_URL=https://api.example.com
Terminal
// ❌ Using server secret on client
'use client'
const key = process.env.STRIPE_SECRET_KEY // Will be undefined!
// ✓ Use NEXT_PUBLIC_ prefix for client
'use client'
const key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
Error Messages
Terminal
// ❌ Verbose errors expose internals
catch (error) {
return Response.json({
error: error.message, // "relation 'users' does not exist"
stack: error.stack // Full stack trace!
})
}
// ✓ Generic errors for clients, detailed logs server-side
catch (error) {
console.error('Database error:', error) // Log full error server-side
return Response.json({
error: 'Something went wrong' // Generic message to client
}, { status: 500 })
}
CSRF Protection
Terminal
// Next.js Server Actions have built-in CSRF protection
// For custom API routes, verify origin:
export async function POST(request: Request) {
const origin = request.headers.get('origin')
const host = request.headers.get('host')
// Verify request origin matches host
if (origin && !origin.includes(host!)) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
// Process request...
}
Rate Limiting
Terminal
// Simple in-memory rate limiting (use Redis for production)
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
})
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, limit, reset, remaining } = await ratelimit.limit(ip)
if (!success) {
return Response.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
}
}
)
}
// Process request...
}
Security Headers
Terminal
// 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: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
}
]
export default {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
]
},
}
Security Review Prompts
Ask AI to Review
Terminal
"Review this code for security vulnerabilities:
[paste code]
Check for:
- SQL injection
- XSS vulnerabilities
- Missing authentication
- Missing authorization
- Hardcoded secrets
- Information disclosure
- Input validation issues
- CSRF vulnerabilities
Provide specific fixes for each issue found."
Generate Secure Code
Terminal
"Generate a secure user registration API endpoint with:
- Input validation using Zod
- Password hashing with bcrypt
- Rate limiting
- Sanitized error messages
- Proper TypeScript types
Follow OWASP best practices."
Security Checklist
Terminal
## Pre-Deployment Security Checklist
### Authentication & Authorization
- [ ] All protected routes verify authentication
- [ ] Authorization checks verify ownership/role
- [ ] Sessions expire appropriately
- [ ] Password requirements enforced
### Input Handling
- [ ] All user input validated server-side
- [ ] SQL queries use parameterization
- [ ] User content sanitized before rendering
- [ ] File uploads validated (type, size)
### Sensitive Data
- [ ] No hardcoded secrets in code
- [ ] Server secrets not exposed to client
- [ ] Errors don't leak internal details
- [ ] PII encrypted at rest
### Headers & Transport
- [ ] HTTPS enforced
- [ ] Security headers configured
- [ ] CORS configured correctly
- [ ] Rate limiting in place
### Monitoring
- [ ] Security events logged
- [ ] Failed login attempts tracked
- [ ] Error monitoring configured
Summary
Security principles for AI code:
- Validate everything: Server-side validation is mandatory
- Sanitize output: Never trust user content
- Authenticate always: Check auth on every request
- Authorize correctly: Verify ownership and roles
- Protect secrets: Use environment variables
- Limit exposure: Generic errors, security headers
- Monitor activity: Log security-relevant events
Next Steps
Explore the future of AI development and emerging trends.
Mark this lesson as complete to track your progress