High Severity CWE-915 OWASP API3:2023

Mass Assignment: When Users Modify Fields They Shouldn't

AI tools generate clean-looking code that lets attackers modify any field

Quick Answer

Mass assignment happens when your API accepts all fields from a request and updates the database without checking which fields users are allowed to modify. Attackers add fields like isAdmin: true to gain elevated privileges. Always use allowlists for updateable fields.

What is Mass Assignment?

Mass assignment (CWE-915) happens when your server blindly saves all fields from a request to the database. Users send extra fields they shouldn't be able to modify, and your server accepts them.

Think of it like an employment form where applicants can write in their own salary. The form accepts whatever they write because nobody checked which fields employees are allowed to fill in.

This vulnerability is so common that it was API6:2019 in OWASP's original API Security Top 10, and has been merged into API3:2023 (Broken Object Property Level Authorization) in the updated list.

Why This Is Devastating

Privilege Escalation

Attackers add isAdmin: true or role: "superuser" to become administrators.

Financial Manipulation

Modify balance, credits, or price fields to steal money or get free services.

Data Corruption

Overwrite createdAt, ownerId, or other system fields to manipulate records.

Account Takeover

Change email without verification, or set emailVerified: true to bypass confirmation.

Real-world example: In 2012, a mass assignment vulnerability in GitHub allowed users to push to any repository by adding their public key to any user's authorized keys list (CVE-2012-2054).

Why AI Tools Generate This Pattern

When you ask AI tools to "create a user update endpoint," they generate the cleanest, most readable code. And in JavaScript, that means Object.assign() or the spread operator. The code looks modern and professional - that's the trap.

AI GENERATES THIS
// Clean, modern, vulnerable code
app.put('/api/user/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
  Object.assign(user, req.body) // "Clean" code
  await user.save()
  res.json(user)
})

The AI optimizes for readability and brevity. Security requires extra code that the AI won't add unless you specifically ask. This is classic vibe coding risk - the generated code works perfectly until someone malicious tests it.

Common vibe coded patterns with mass assignment:

  • User profile updates - "Let users update their profile"
  • Settings pages - "Add user preferences"
  • Admin panels - "Create CRUD for users"
  • API endpoints - "Build REST API for posts"

Vulnerable Code Examples

Pattern 1: Object.assign (AI Default)

VULNERABLE
// AI generates this for "update user profile"
app.put('/api/user/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
  Object.assign(user, req.body) // DANGEROUS!
  await user.save()
  res.json(user)
})

// Attacker sends:
// { "name": "John", "isAdmin": true, "role": "superuser" }
// All fields get saved, including isAdmin!

Object.assign copies ALL properties from request body. Attacker can modify any field in the schema.

Pattern 2: Spread Operator

VULNERABLE
// Spread operator is just as dangerous
app.put('/api/user/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
  const updated = { ...user.toObject(), ...req.body } // DANGEROUS!
  await User.findByIdAndUpdate(req.params.id, updated)
  res.json(updated)
})

// Same attack vector as Object.assign

The spread operator (...) merges objects the same way. No protection against extra fields.

Pattern 3: Prisma Update

VULNERABLE
// Passing request body directly to Prisma
app.put('/api/user/:id', async (req, res) => {
  const user = await prisma.user.update({
    where: { id: req.params.id },
    data: req.body, // DANGEROUS!
  })
  res.json(user)
})

// Attacker can set any column in the users table

Prisma accepts any fields matching the schema. No automatic field filtering.

How to Fix Mass Assignment

Secure Pattern: Explicit Allowlist

SECURE
// Explicitly pick allowed fields
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'avatar']

app.put('/api/user/:id', async (req, res) => {
  const user = await User.findById(req.params.id)

  // Only copy allowed fields
  for (const field of ALLOWED_UPDATE_FIELDS) {
    if (req.body[field] !== undefined) {
      user[field] = req.body[field]
    }
  }

  await user.save()
  res.json(user)
})

// Attacker's isAdmin: true is ignored

Explicitly define which fields users can modify. Ignore everything else.

Secure Pattern: Zod Validation

SECURE
// Use Zod to define and validate allowed fields
import { z } from 'zod'

const UpdateUserSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.string().email().optional(),
  avatar: z.string().url().optional(),
})

app.put('/api/user/:id', async (req, res) => {
  // Parse and validate - strips unknown fields
  const data = UpdateUserSchema.parse(req.body)

  const user = await prisma.user.update({
    where: { id: req.params.id },
    data, // Only contains allowed, validated fields
  })

  res.json(user)
})

Zod strips unknown fields by default. Only schema-defined fields pass through.

Secure Pattern: Destructuring

SECURE
// Destructure only allowed fields
app.put('/api/user/:id', async (req, res) => {
  const { name, email, avatar } = req.body

  const user = await prisma.user.update({
    where: { id: req.params.id },
    data: {
      // Only set fields you explicitly allow
      ...(name && { name }),
      ...(email && { email }),
      ...(avatar && { avatar }),
    },
  })

  res.json(user)
})

Destructure specific fields from the request. Only those fields can be updated.

Secure Pattern: Next.js Server Actions

SECURE
// Next.js Server Action with field allowlist
'use server'

import { z } from 'zod'

const UpdateProfileSchema = z.object({
  name: z.string().min(1).max(100),
  bio: z.string().max(500).optional(),
})

export async function updateProfile(formData: FormData) {
  const rawData = Object.fromEntries(formData)

  // Validate and strip unknown fields
  const data = UpdateProfileSchema.parse(rawData)

  await prisma.user.update({
    where: { id: session.userId },
    data,
  })
}

Server Actions with Zod validation ensure only allowed fields are processed.

Key Security Points

  • Always use allowlists - explicitly list which fields are updateable
  • Never use blocklists - "block isAdmin" misses new dangerous fields
  • Validate with schemas - Zod strips unknown fields automatically
  • Different allowlists per role - admins may update more fields than users
  • Audit sensitive fields - role, isAdmin, balance, permissions need extra protection

AI Fix Prompt

Copy this prompt to your AI tool to scan your codebase for mass assignment vulnerabilities:

Mass Assignment Audit Prompt

Frequently Asked Questions

Related content

External Resources

Find Mass Assignment in Your Code

VibeShip Scanner automatically detects mass assignment vulnerabilities in your codebase, including Object.assign, spread operators, and direct ORM updates.

Scan Your Code Free