Mesrai
Back to blog
// essaySecurity

IDOR in REST Endpoints: The /users/:id Diff Mesrai Caught

Real PR catch: `/users/:id` with no ownership check — sequential ids let any user scrape the entire user table. CWE-639 walkthrough.

Mesrai TeamJuly 25, 20268 min read

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

tsapi/users.ts
// 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:

bashexploit
# 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

textreview-comment.txt
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-639

The fix

tsapi/users.ts (fixed)
// 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.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.