API Security in Depth · API Threat Landscape & Object-Level Risks
Object Property Level Authorization: Mass Assignment & Data Exposure
Control which fields each caller may read and write.
BOPLA merges two classic issues: mass assignment (client sets fields it shouldn't, like role, balance, verified) and excessive data exposure (responses include internal fields like password_hash, internal_notes, other users' emails). The fix is explicit schemas per operation and role: separate input DTOs that whitelist writable fields, and output DTOs that whitelist readable fields. Never bind requests directly to ORM entities or serialize entities directly to JSON.
Binding strategies
| Criterion | Entity in / entity out | DTOs per operation | DTOs per operation + role |
|---|---|---|---|
| Mass assignment risk | High | Low | Low |
| Data exposure risk | High | Low | Lowest |
| Boilerplate | None | Some | More |
| Schema docs accuracy | Poor | Good | Good |
| Use when | Never for public APIs | Default | Admin vs user views differ |
class ProfileUpdate(BaseModel):
model_config = ConfigDict(extra="forbid") # reject unknown fields (e.g. "role")
display_name: str | None = Field(None, max_length=80)
bio: str | None = Field(None, max_length=500)
class ProfileOut(BaseModel): # whitelist of readable fields
id: UUID
display_name: str
bio: str | None
class AdminProfileOut(ProfileOut):
email: EmailStr
created_at: datetime
const ProfileUpdate = z.object({
displayName: z.string().max(80).optional(),
bio: z.string().max(500).optional(),
}).strict(); // unknown keys → error
app.patch("/v1/me", requireAuth, async (req, res) => {
const input = ProfileUpdate.parse(req.body);
const user = await prisma.user.update({ where: { id: req.user.id }, data: input });
res.json({ id: user.id, displayName: user.displayName, bio: user.bio });
});
public record ProfileUpdate(@Size(max = 80) String displayName, @Size(max = 500) String bio) {}
public record ProfileOut(UUID id, String displayName, String bio) {
static ProfileOut from(User u) { return new ProfileOut(u.getId(), u.getDisplayName(), u.getBio()); }
}
// spring.jackson.deserialization.fail-on-unknown-properties=true
Common pitfalls
Object.assign(user, req.body)orModel(**request.json).- Filtering sensitive fields in the frontend instead of the API.
- GraphQL types exposing every column by default.
Key points
- Separate input and output schemas
- Reject unknown fields
- Different output schemas for different roles
Further reading: OWASP API3:2023 BOPLA
Practice questions
What is mass assignment?
Answer
Binding client input directly to internal objects so attackers can set fields they shouldn't (e.g. role, balance) by adding them to the request.
Why is 'the UI doesn't show that field' not protection against data exposure?
Answer
Attackers read raw API responses; anything in the JSON is exposed regardless of what the UI renders.
Open in the app to mark lessons complete, take notes and track your progress.