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