Soft-Deleted Records Still Accessible: The Quiet IDOR Class
The UI says “Deleted.” The list endpoint filters deleted_at IS NOT NULL. Direct GET /items/{id} still returns the full JSON. That gap is a soft-delete IDOR (or retention bypass), and it shows up constantly in SaaS apps with recycle bins, GDPR erase buttons that only hide rows, and mobile clients that cache old IDs.
Why it pays
Deleted content is often more sensitive: drafted messages someone regretted, old payroll exports, revoked API keys still in the row, files legal wanted removed. If any user who once knew the ID — or any other tenant, in the worse cases — can fetch it, your impact writeup writes itself.
Hunting pattern
- Create an object as Account A. Capture its ID.
- Delete via UI or
DELETEAPI. Confirm list views omit it. - Replay
GET,PATCH, download, share, and GraphQL node queries with the same ID. - Repeat from Account B’s session for cross-tenant severity.
Watch for 200 with body, 301 to a CDN URL, or “tombstone” objects that still include PII in fields the UI ignores.
Restore and race angles
Some products offer “restore within 30 days.” Test whether restore endpoints accept another user’s IDs. Race delete vs read: workers that generate PDFs may still run after soft delete and drop artifacts in a public bucket. I’ve also seen search indexers keep serving soft-deleted documents for hours — separate finding, same theme.
Admin vs user confusion
Staff tools may intentionally read deleted rows. The bug is user-role tokens doing the same. Check role=user sessions only. If only support roles succeed, document it as intended privilege and move on.
Proof checklist
- Timestamps:
deleted_atpresent while confidential fields still populated - Side endpoints:
/download,/export,/commentsnested under the deleted parent - Webhooks that still fire with full payloads after delete
- “Empty trash” that removes from UI but leaves object storage keys resolvable
Remediation you can recommend
Enforce the same delete filter on every read path, or hard-delete / encrypt-at-rest after tombstone. Cascade access checks to children. Treat storage URLs as revoked when the parent soft-deletes (rotate object keys). Log access to deleted IDs for abuse detection.
Frame with CWE-639 and the program’s data-retention expectations. If the target markets “delete means delete,” quote that marketing next to your 200 OK.
Report title examples
“Soft-deleted invoices still readable via GET /api/invoices/{id}” beats “IDOR on invoices.” Specificity gets faster accept.
Not every missing list filter is critical. Cross-user read of deleted KYC documents is. Match severity to the data, then ask whether the delete control was a security boundary or only a UX hide.