← Back to Attack Research

One DELETE to erase everyone: the under-gated destructive endpoint

Your read endpoints are authorization-hardened. Your DELETE route is one line of code nobody tenant-scoped, and a single request erases every tenant's sources, agents, and assessments at once. Destructive operations are routinely under-gated relative to reads. Here is the decision tree from finding the destructive endpoint to platform-wide destruction, grounded in CVE-2026-53469, and the class test that covers every write and delete route.

A user with a low-privilege account on your SaaS finds an endpoint you forgot to gate. Not a read endpoint, those got reviewed. A DELETE. They send one request: DELETE /api/v1/sources, no resource id, no tenant filter, no confirmation. The handler runs an unscoped delete and every customer's sources, agents, and assessments are gone in one round trip. That is CVE-2026-53469 (CVSS 9.1), disclosed against the kubev2v migration-planner with Red Hat as the CNA: a DELETE route that "lacks proper authorization and filtering," so an authenticated request destroys data across the whole platform. The class is CWE-306, Missing Authentication for Critical Function. The lesson is not about one product. It is that destructive operations are under-gated relative to reads almost everywhere, and the blast radius is availability and integrity for every tenant at the same time.

Security review has a blind spot shaped like a verb. Teams pour authorization scrutiny into GET endpoints because that is where data exfiltration lives, and a decade of IDOR research trained everyone to chase read access. Meanwhile the POST, PUT, PATCH, and DELETE handlers, the ones that change or destroy state, get a fraction of the same attention. The asymmetry is backwards. A read-IDOR leaks one record at a time. An unauthorized DELETE with no tenant filter erases everyone at once, and there is no record to leak afterward because there is no record.

This is part of our attack-research series on the classes that ship despite a decade of patches. Earlier pieces walked cross-tenant read and JWT confusion as classes. This one walks destructive-operation authorization the same way: not as one CVE, but as a family of write and delete routes that inherited a fraction of the authorization their sibling read routes received. By the end you should be able to enumerate every state-changing route in your own surface, ask the two questions that decide whether it is safe, and ship the controls that make a platform-wide wipe structurally impossible. Verifiable security.

The attack pattern in one paragraph

An API exposes a destructive operation: a route whose handler deletes, overwrites, or disables persistent state. The attacker does not need to forge a token or escalate to admin; they need only an authenticated session of any kind, because the destructive route never checks which caller is allowed to invoke it or which rows it is allowed to touch. Two gates are missing. The first is the authorization gate: the handler authenticates that a session exists but never authorizes that this session may perform a destructive action on these resources. The second is the tenant-scope gate: the delete query has no WHERE tenant_id = ? predicate, so it operates over the entire table rather than the caller's slice. When both gates are missing on the same route, a single request with no resource id deletes the global collection. The read endpoints on the same service may be perfectly scoped, which is exactly why the gap survives review: the team verified the routes that leak and never re-verified the route that destroys.

The unifying observation: destructive operations are gated like the read endpoints next to them, when they should be gated harder. The attacker lives in the assumption that a route which got authorization for reads inherited it for deletes.

Why this still ships in 2026

If broken authorization is the most-studied API risk class, with its own entry as API5:2023 Broken Function Level Authorization in the OWASP API Security Top 10, why does a single unauthorized DELETE still erase a whole platform? Four structural reasons.

  1. Reads get the authorization budget, writes get the leftovers. Penetration tests and code reviews are framed around data exposure, so reviewers fan out across GET handlers checking object ownership. The DELETE handler three lines down gets a glance. OWASP names this exact pattern: regular users reaching administrative or sensitive operations because "exposed endpoints will be easily exploited" when function-level checks are inconsistent across verbs.
  2. The collection-level destructive route looks harmless because it is rarely called. A DELETE /api/v1/sources/{id} with an id is obviously dangerous and usually gated. A bare DELETE /api/v1/sources on the collection often exists as a developer convenience, a test-reset, or an internal cleanup that was never meant to ship reachable. It does not appear in the UI, so it does not appear in the threat model, so it never gets the ownership check that the per-resource route eventually received.
  3. Authentication is mistaken for authorization. CWE-306 is precisely the failure to perform any authorization for a function that consumes significant resources or requires a provable identity. Many destructive handlers verify a valid session exists and stop there. A valid session is not a grant. The migration-planner finding is exactly this shape: an authenticated user reaches a route that "lacks proper authorization and filtering," and authentication alone was never the control that should have stood between them and a global delete.
  4. Hard deletes leave no evidence and no undo. When the destructive query is a real DELETE rather than a soft-delete flag, there is no row left to audit, no tombstone to recover from, and frequently no audit-log line because logging was wired into the read path. The first signal the platform owner gets is every tenant reporting their data is gone at once. Integrity and availability fail together, globally, in one request.

The attacker decision tree

ATTACKER DECISION TREE Under-Gated Destructive Endpoint ┌──────────────────────────────────────────┐ │ 1. Get any authenticated session │ │ - sign up / low-priv account / token │ │ - no admin, no escalation needed │ └────────────────┬─────────────────────────┘ │ "reads are hardened, so attack │ the verbs nobody re-reviewed" ▼ ┌──────────────────────────────────────────┐ │ 2. Enumerate STATE-CHANGING routes │ │ POST / PUT / PATCH / DELETE │ │ - from API spec / JS bundle / docs │ │ - probe collection paths w/o {id} │ │ - DELETE /api/v1/sources <-- bare │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 3. Ask the two gate questions │ │ a) AUTHZ? does it check WHO may delete │ │ not just THAT a session exists │ │ b) SCOPE? does the query filter to MY │ │ tenant, or run over the whole table │ └────────────────┬─────────────────────────┘ │ both gates missing ▼ ┌──────────────────────────────────────────┐ │ 4. One request = platform-wide wipe │ │ DELETE /api/v1/sources (no id) │ │ -> sources, agents, assessments gone │ │ -> EVERY tenant, not just mine │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 5. No undo, no audit row, no evidence │ │ hard delete = integrity + availability│ │ fail together, globally, at once │ └──────────────────────────────────────────┘

The five-step tree an attacker walks from any authenticated session to a platform-wide destruction with one request.

The decisive move at step 3 is that the attacker asks two questions, not one. A reviewer who only asks "is this endpoint authenticated?" gets a reassuring yes and moves on. The attacker asks "does it authorize the destructive action, and does it scope the rows to me?" When the answer to both is no, the bare collection path needs no id and no payload: the absence of a filter is the exploit. Enumeration is fast and mostly passive: read the OpenAPI spec, diff the JavaScript bundle for fetch calls, probe the collection path of every resource the per-id route exposes. The route that accepts a verb-only request with no scoping is the win.

A composite real-world scenario

The setting is a migration-assessment SaaS. Customers register infrastructure sources, deploy collector agents against them, and receive assessments that recommend a migration path. Tenants are isolated by an org id on every row. The read API is exemplary: GET /api/v1/sources filters by the caller's org, GET /api/v1/sources/{id} checks ownership before returning, and a penetration test last quarter confirmed no cross-tenant read. The team is proud of it, and they should be.

An attacker creates a free evaluation account and lands as a normal tenant user. They pull the API surface from the published OpenAPI document and notice the resource collections expose more verbs than the UI uses. They walk the destructive verbs against each collection path with no resource id.

# Enumerate destructive verbs on the bare collection paths.
# A valid session token, low-privilege, is the only credential.
$ TOKEN="eyJ...evaluation-account..."

$ curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
    -H "Authorization: Bearer $TOKEN" \
    https://app.example/api/v1/sources
# 200   <-- not 401, not 403, not 404. It ran.

The route returned 200. It did not reject the caller, and it did not ask for a resource id. The attacker now asks the two gate questions against the observed behaviour: authorization is absent, because a low-privilege evaluation account was allowed to invoke a destructive operation; tenant scope is absent, because there was no id in the path to scope to, and the handler did not substitute the caller's org id. The handler is, in effect, the unscoped delete shown below.

# The handler that ships the vulnerability (illustrative).
@app.delete("/api/v1/sources")
def delete_sources(session=Depends(require_session)):
    # require_session AUTHENTICATES. It does not AUTHORIZE,
    # and it does not pass a tenant filter to the query.
    db.execute("DELETE FROM sources")           # whole table
    db.execute("DELETE FROM agents")            # cascade, all tenants
    db.execute("DELETE FROM assessments")       # cascade, all tenants
    return {"status": "ok"}

One request erased every tenant's sources, every agent attached to them, and every assessment derived from them. The attacker's own org was destroyed too; that is irrelevant to them and catastrophic to the platform. There was no per-resource ownership check to fail, because the route never loaded a resource. There was no tenant filter to satisfy, because the query named a table, not a slice of one. The read endpoints next door were textbook-correct the entire time. That is the trap: the correctness of the read path is taken as evidence that the service is authorization-sound, and the destructive route inherited none of it. This is the precise shape of CVE-2026-53469, where an authenticated user sending a DELETE to the sources route, which "lacks proper authorization and filtering," destroys all customer data and produces critical availability and integrity loss across the platform.

Why read-focused testing misses this, and how a class test catches it

The reason this slips past review is structural, not careless. Authorization testing is usually organised by resource and by data exposure: for each object, can a user read an object they do not own? That framing fans out across read paths and per-resource handlers. It does not naturally ask, for each state-changing route, whether an under-privileged caller can invoke it and whether it scopes to the caller. The bare collection-level destructive route is invisible to a resource-by-resource read audit because it touches no single resource.

A class test inverts the framing. Instead of auditing objects for read exposure, it enumerates every route whose verb changes state, namely every POST, PUT, PATCH, and DELETE, from the API specification, and for each one asks the same two questions with an under-privileged identity: does it reject a caller who lacks the destructive grant, and does it confine its effect to the caller's tenant? The test does not need to actually destroy anything to find the gap; on a fixture it can assert that an unauthorized caller receives 403 and that the query carries a tenant predicate. Run as a class, it covers the convenience routes, the test-reset routes, and the internal-cleanup routes that a UI-driven or read-driven audit never reaches. The differentiator is auditing destructive operations as a class, not chasing read-IDOR one object at a time.

What to do about it: the destructive-operation contract

The fix is not one line, because under-gating is a property of how the surface was reviewed, not a single missing check. But it reduces to a contract every state-changing route should satisfy, and the controls are cheap.

Destructive-operation contract: gate every route that changes state

A read-IDOR leaks one record. An unauthorized DELETE with no tenant filter erases everyone at once, and there is no record left to leak.

The audit, in concrete terms, starts by listing every state-changing route from the specification and probing each collection path with an under-privileged identity:

# Enumerate every destructive route from the OpenAPI spec
$ jq -r '.paths | to_entries[]
    | .key as $p | .value | to_entries[]
    | select(.key | test("post|put|patch|delete"))
    | "\(.key | ascii_upcase) \($p)"' openapi.json

# For each collection-level destructive path, assert an
# under-privileged caller is DENIED (expect 401/403, never 200)
$ for path in $(...collection paths...); do
    code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
      -H "Authorization: Bearer $LOW_PRIV_TOKEN" "$BASE$path")
    [ "$code" = "200" ] && echo "UNGATED DESTRUCTIVE: DELETE $path"
  done

Read each flagged route. Confirm the handler authorizes the destructive action and that the query carries a tenant predicate. Confirm collection-level destruction requires an explicit target. The exercise is finishable in a day for a single service, and it covers the routes a read audit never sees.

How Celvex catches this

Find. Prove. Fix. Verify.

Find

The scanner enumerates every state-changing route from the API surface and, with an under-privileged identity, checks each destructive path for the two missing gates: absent authorization and absent tenant scope. Probes are read-safe and carry an X-Celvex-Probe header so the customer's SOC can always identify our traffic.

Prove

For a confirmed ungated destructive route we ship a signed Proof Capsule with a fixture that reproduces the gap offline: the route, the under-privileged token accepted, and the query that would run over the whole table, with no real data destroyed.

Fix

The Capsule's remediation block points at the destructive-operation control that failed: the authorization check to add, the tenant predicate to inject, the confirmation to require, or the soft-delete migration that makes the wipe recoverable.

Verify

After the fix lands, the under-privileged destructive call is denied at 403 and the query carries a tenant filter. The finding closes automatically and the dashboard records the verified-fix event for the audit trail.

This is the differentiator: we audit destructive operations as a class through our vulnerability validation track, rather than chasing read-IDOR one object at a time. Where we sit on the autonomy curve: at L1.5 today, the track enumerates state-changing routes from the API spec and flags collection-level destructive paths that accept an under-privileged caller. At L2 within 90 days, the corpus extends the under-privileged probe to PUT and PATCH overwrite routes and to bulk-mutation endpoints, the same missing-gate primitive on non-delete verbs. At L3 within twelve months, the scanner synthesises route-specific destructive probes for unfamiliar API shapes it fingerprints in customer environments. We do not claim L3 today. We do claim our L1.5 finds the ungated collection-level delete and ships a reproducible Capsule for it.

Bottom line

The most expensive authorization bug on your surface is probably not a read. It is a write or a delete that inherited a fraction of the scrutiny the read endpoints next to it received, on a route that runs over the whole table because nobody scoped it to a tenant. CVE-2026-53469 is the 2026 reminder: one authenticated DELETE /api/v1/sources with no authorization and no filter erased every tenant's sources, agents, and assessments at once, and CWE-306 is the class name. The fix is a contract: authorize every state-changing operation, scope every write and delete to the caller's tenant, require an explicit target and confirmation for collection-level destruction, prefer soft-delete, audit every destructive call, and test destructive routes as a class on every change. Until you gate the verbs that destroy as hard as the verbs that read, a green read-authorization report is one bare DELETE away from a platform-wide wipe.

Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.

Sources

Audit your destructive routes as a class.

Free Exposure Check, no signup required. We enumerate every state-changing route on your surface and ship a Proof Capsule for the highest-confidence ungated destructive operation.

Run a Free Scan →