A user-profile endpoint at `/users/:id`. Returns the full user object including email, phone, address, and last-login IP. Auth required. Mesrai flagged it because 'logged-in' is not the same access control as 'allowed to see this profile'.
The vulnerable diff
// api/users.ts
app.get("/users/:id", requireAuth, async (req, res) => {
// BUG: no ownership or role check; full record returned
const u = await db.users.findUnique({
where: { id: Number(req.params.id) },
});
if (!u) return res.status(404).end();
res.json(u); // returns email, phone, address, last_login_ip, …
});What is wrong
IDOR is a specialization of CWE-862 specifically for direct object references — usually URL path or query parameters that map to a primary key. CWE-639 is the canonical entry. Sequential integer ids make enumeration trivial; even non-sequential ids leak through analytics, social profiles, and support tickets. Returning the full user record amplifies the damage: every field harvested by the attacker is potential PII for downstream attacks.
The attack
A naive scraping loop:
# 30 lines of Python, run from the attacker's personal account
for uid in range(1, 1_000_000):
r = requests.get(f"https://api.example.com/users/{uid}",
cookies={"sid": MY_SESSION})
if r.status_code == 200:
save(r.json()) # email, phone, address, last_login_ip
# Throttle as needed to avoid rate limits — most apps don't have any.Result: an exfiltrated user database. The PII is then used in phishing, credential stuffing, OSINT, sometimes resale on darknet markets.
Mesrai's review comment
mesraipilot · Bot · reviewed 45 sec ago
[mesrai] [code-review] [Security] [CWE-639] [critical]
`/users/:id` returns the full user record to any logged-in user. This
is IDOR — the URL exposes a direct object reference with no
authorization beyond "is logged in".
Two parallel fixes. Pick based on the actual product requirement:
A) If users only ever need their own data, drop the param:
app.get("/users/me", requireAuth, async (req, res) => {
const u = await db.users.findUnique({
where: { id: req.user.id },
select: PUBLIC_FIELDS,
});
res.json(u);
});
B) If users can see other public profiles, gate by role + return
only public fields:
app.get("/users/:id", requireAuth, async (req, res) => {
const u = await db.users.findUnique({
where: { id: Number(req.params.id) },
select: PUBLIC_FIELDS, // never email/phone/address/IP
});
if (!u) return res.status(404).end();
res.json(u);
});
Add rate-limiting on this endpoint regardless — enumeration is
slowed but not stopped by per-user limits.
Reference: OWASP API Security Top 10 — API1: Broken Object Level
Authorization, CWE-639The fix
// api/users.ts — fixed (option B with field projection)
const PUBLIC_FIELDS = { id: true, name: true, avatarUrl: true } as const;
app.get("/users/:id", requireAuth, async (req, res) => {
const u = await db.users.findUnique({
where: { id: Number(req.params.id) },
select: PUBLIC_FIELDS,
});
if (!u) return res.status(404).end();
res.json(u);
});Two changes. Restrict the response to non-sensitive fields — `name` and `avatar` are typically OK for public profiles, `email` and `phone` are not. Apply rate limiting at the gateway so even the reduced payload cannot be scraped en masse.
Why human review missed it
IDOR is one of OWASP's most-reported API vulnerabilities for a reason: the bug looks like ordinary REST. Every team has a `/users/:id` endpoint. The fix is a checklist (auth + ownership + field projection + rate limit), and the bug appears whenever any of those four is missed. Mesrai's rule pack catches handlers that return user-shaped records from a request-id parameter without projection.
Related rules + further reading
Mesrai rule pack: security/idor-user-fields — flags handlers returning full user records by request id without field projection.
OWASP: API Security Top 10 — API1: Broken Object Level Authorization.
CWE-639 — Authorization Bypass Through User-Controlled Key.
Takeaway
Either restrict by /me, or gate by role + project fields. Plus rate limit. Mesrai checks all four boxes on every user-shaped endpoint.