DEV Community

Atlas Whoff
Atlas Whoff

Posted on

OWASP Top 10 for Next.js: SQL Injection, XSS, IDOR, and Mass Assignment Prevention

Most developers know the OWASP Top 10 exists. Fewer have actually read it. Fewer still have checked their own codebase against it. Here's a practical walkthrough of the most common vulnerabilities in Next.js apps and how to prevent them.

1. Injection (SQL, Command, NoSQL)

// VULNERABLE -- string interpolation in raw SQL
const users = await db.$queryRaw(`SELECT * FROM users WHERE email = '${email}'`)

// SAFE -- parameterized query
const users = await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`

// SAFE -- Prisma ORM
const users = await db.user.findMany({ where: { email } })

// VULNERABLE -- command injection
exec(`convert ${filename} output.pdf`) // filename could be '; rm -rf /'

// SAFE -- use a library, validate inputs
import { execFile } from 'child_process'
execFile('convert', [sanitizedFilename, 'output.pdf'])
Enter fullscreen mode Exit fullscreen mode

2. Broken Authentication

// VULNERABLE -- weak session secrets
NEXTAUTH_SECRET=secret123

// SAFE -- 32+ char random secret
NEXTAUTH_SECRET=$(openssl rand -base64 32)

// VULNERABLE -- JWT without expiry
jwt.sign({ userId }, secret) // never expires

// SAFE -- short-lived tokens
jwt.sign({ userId }, secret, { expiresIn: '15m' })
Enter fullscreen mode Exit fullscreen mode

3. Sensitive Data Exposure

// VULNERABLE -- returning full user object
return Response.json(user) // includes password hash, PII

// SAFE -- explicit field selection
const { id, name, email } = user
return Response.json({ id, name, email })

// VULNERABLE -- logging sensitive data
console.log('User data:', user) // logs password hash

// SAFE -- log only safe fields
logger.info({ userId: user.id, action: 'login' })
Enter fullscreen mode Exit fullscreen mode

4. Insecure Direct Object References

// VULNERABLE -- anyone can access any document
const doc = await db.document.findUnique({ where: { id: params.id } })

// SAFE -- scope to authenticated user's org
const doc = await db.document.findFirst({
  where: {
    id: params.id,
    organizationId: session.user.organizationId, // ownership check
  },
})
if (!doc) return Response.json({ error: 'Not found' }, { status: 404 })
Enter fullscreen mode Exit fullscreen mode

5. Cross-Site Scripting (XSS)

// VULNERABLE -- dangerouslySetInnerHTML with user content
<div dangerouslySetInnerHTML={{ __html: userContent }} />

// SAFE -- sanitize before rendering
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />

// BETTER -- use a markdown renderer with sanitization
import { marked } from 'marked'
import { sanitize } from 'dompurify'
const html = sanitize(marked(userMarkdown))
Enter fullscreen mode Exit fullscreen mode

React's JSX escapes by default. dangerouslySetInnerHTML bypasses this — treat it as a security boundary.

6. Security Misconfiguration

// next.config.js -- security headers
const securityHeaders = [
  { key: 'X-DNS-Prefetch-Control', value: 'on' },
  { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // tighten in production
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
    ].join('; '),
  },
]

module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}
Enter fullscreen mode Exit fullscreen mode

7. Mass Assignment

// VULNERABLE -- spread request body directly
await db.user.update({
  where: { id },
  data: { ...req.body }, // attacker can set isAdmin: true
})

// SAFE -- explicit allowed fields with Zod
const schema = z.object({
  name: z.string().max(100),
  bio: z.string().max(500).optional(),
  // role, isAdmin, stripeCustomerId NOT included
})
const data = schema.parse(req.body)
await db.user.update({ where: { id }, data })
Enter fullscreen mode Exit fullscreen mode

The MCP Security Scanner at whoffagents.com scans MCP servers for injection, path traversal, command execution, and 19 other vulnerability classes. $29 one-time.

Top comments (0)