Topic: mcp server mass assignment
MCP server mass assignment — bulk model update vulnerabilities in tool handlers
Mass assignment is a vulnerability class where an application applies a bulk update to an object — a database model, an internal configuration record — using a set of key-value pairs supplied by an external caller, without first filtering that set to only the fields the caller is permitted to update. In an MCP server, the "external caller" is the AI model supplying tool arguments. A tool that accepts an updates object and spreads it directly onto a database record allows the AI (or a prompt-injection attack that controls the AI's output) to set security-sensitive fields — isAdmin, role, permissions, stripeCustomerId — that the tool was never meant to expose.
The mass assignment pattern in MCP server tool handlers
Mass assignment is most common in tools that expose a generic "update record" interface — updating a user profile, editing a document, modifying a task. The vulnerable pattern almost always involves the spread operator or an ORM's update(data) call receiving unfiltered input.
// VULNERABLE: spread of AI-supplied args directly onto Prisma update
server.tool('updateUserProfile', z.object({
userId: z.string(),
updates: z.record(z.unknown()), // accepts ANY key-value pairs
}), async ({ userId, updates }) => {
// If updates = { isAdmin: true, subscriptionTier: 'enterprise' }
// these fields get written to the database
const user = await prisma.user.update({
where: { id: userId },
data: { ...updates }, // MASS ASSIGNMENT — isAdmin can be set here
})
return user
})
// The AI model is not the only threat: a prompt injection in
// a user-supplied document that the AI reads can instruct it to
// call updateUserProfile({ userId: '...', updates: { isAdmin: true } })
Four safe patterns for MCP server update handlers
1. Explicit allowlist of updatable fields
// SAFE: explicit schema for exactly the fields the tool should update
server.tool('updateUserProfile', z.object({
userId: z.string(),
displayName: z.string().max(100).optional(),
bio: z.string().max(500).optional(),
timezone: z.string().optional(),
// isAdmin, role, subscriptionTier are NOT in this schema
// The AI cannot supply them regardless of handler implementation
}), async ({ userId, displayName, bio, timezone }) => {
const updateData: Record = {}
if (displayName !== undefined) updateData.displayName = displayName
if (bio !== undefined) updateData.bio = bio
if (timezone !== undefined) updateData.timezone = timezone
const user = await prisma.user.update({
where: { id: userId },
data: updateData,
})
return { id: user.id, displayName: user.displayName }
})
2. Pick helper to extract only the allowed fields
// For cases where you accept a broader input object and must filter it:
const UPDATABLE_FIELDS = ['displayName', 'bio', 'timezone', 'locale'] as const
function pickAllowedUpdates(input: Record) {
return Object.fromEntries(
UPDATABLE_FIELDS
.filter(key => input[key] !== undefined)
.map(key => [key, input[key]])
)
}
server.tool('updateUserProfile', z.object({
userId: z.string(),
updates: z.record(z.unknown()),
}), async ({ userId, updates }) => {
const safeUpdates = pickAllowedUpdates(updates)
if (Object.keys(safeUpdates).length === 0) {
return { error: 'No valid updatable fields provided' }
}
const user = await prisma.user.update({
where: { id: userId },
data: safeUpdates,
})
return { id: user.id, displayName: user.displayName }
})
3. Separate read schema from write schema
import { z } from 'zod'
// Full user record — returned by read operations
const UserSchema = z.object({
id: z.string(),
displayName: z.string(),
email: z.string(),
isAdmin: z.boolean(),
role: z.enum(['user', 'editor', 'admin']),
subscriptionTier: z.enum(['free', 'pro', 'team']),
createdAt: z.string(),
})
// Updateable subset — the ONLY fields the AI can write via this tool
const UserUpdateSchema = z.object({
displayName: z.string().max(100).optional(),
bio: z.string().max(500).optional(),
timezone: z.string().optional(),
})
// isAdmin, role, subscriptionTier are not in UserUpdateSchema
// They can only be set via separate administrative tools with
// their own authorization guards
type UserUpdate = z.infer<typeof UserUpdateSchema>
server.tool('updateUserProfile', z.object({
userId: z.string(),
updates: UserUpdateSchema,
}), async ({ userId, updates }) => {
const user = await prisma.user.update({
where: { id: userId },
data: updates,
})
return { id: user.id, displayName: user.displayName }
})
4. Structured update log for forensics
// Log every update with the exact fields changed and their previous values
async function auditedUpdate(
entityType: string,
entityId: string,
updates: Record,
previousValues: Record,
) {
const changedFields = Object.keys(updates).filter(
key => updates[key] !== previousValues[key]
)
console.log(JSON.stringify({
event: 'entity_updated',
entityType,
entityId,
changedFields,
timestamp: new Date().toISOString(),
// Do NOT log the values themselves if they might contain PII
// Log only the field names and whether the value was null/non-null
fieldNullability: Object.fromEntries(
changedFields.map(f => [f, updates[f] === null ? 'null' : 'non-null'])
),
}))
}
What SkillAudit checks
- Spread of tool argument objects directly onto ORM update calls — HIGH; if the ORM schema has security-sensitive fields (
isAdmin,role,permissions, columns matchingadmin/super/privilegepatterns) reachable via mass assignment z.record(z.unknown())or equivalent in tool schemas feeding into ORM update operations — WARN; schema design allows arbitrary keys that may reach security-sensitive fields- ORM
update(data)calls wheredatais directly derived from tool arguments without an explicit allowlist extraction — WARN; even with schema validation, dynamic ORM fields can include unexpected columns - Missing audit logging on multi-field update operations — INFO; absence of field-level change logs makes post-incident forensics difficult
See also
- MCP server input validation — schema-first argument design with Zod
- MCP server role-based access control — authorization guard patterns for tool handlers
- MCP server audit logging — structured logging for security-sensitive operations
- MCP server permissions checklist — field-level permission hygiene in the broader permissions context
Check your MCP server for mass assignment vulnerabilities in update tool handlers.
Run a free audit → How grading works →