Skip to main content
RuyaTech
Security
MVP Rescue
AI Development

When the server trusts the client to say who you are

Oussama IbrahimFounder & Lead Engineer5 min read

Key takeaway: The most common critical bug in AI-built marketplaces is not injection or a leaked key. It is mutation endpoints that read the caller's identity from the request body instead of the verified session. The fix: derive identity only from the session, strip user-id fields from request schemas, and check ownership before every write.

A marketplace MVP I read recently had a complete server-side OIDC pipeline. PKCE, nonce, state, refresh tokens, secure cookies, the works. It also had a frontend that sent the user id from localStorage on every API call, and never imported the auth library at all.

Two halves of an authentication system that never met in the middle.

That gap is interesting, but it isn't the finding. The finding is what fell through it. Once you stop assuming the frontend will tell the truth about who is calling, you start reading the API the way an attacker would, and a pattern shows up that I see in almost every fast-built marketplace.

The server trusted the request body to say who the caller was.

This bug has names. It is broken access control, often called an Insecure Direct Object Reference (IDOR) or Broken Object Level Authorization (BOLA), and it sits at the top of the OWASP API Security Top 10. The names matter less than the shape, which is always the same.

What that looks like in code

The API took the caller's identity (renterId, hostId) straight from the request body, so a stranger with curl could create, approve, and flip records as anyone.

POST /api/bookings accepted a renterId field in the JSON body and wrote it straight to the bookings table. POST /api/listings accepted a hostId. PATCH /api/bookings/:id/status validated the enum and confirmed the row existed, then wrote the new status. None of these routes called the auth middleware. None of them compared an authenticated user id against the resource they were touching.

In plain terms: a stranger with curl could create a booking as you, approve it as the host you were booking from, and flip its status to approved, all in three requests, none of them logged in.

The privacy gate that made it worse

The host's home address was gated on a single status flag, with no check that the caller was the renter or host, so the mutation bug turned into a full address leak.

The product had one piece of sensitive data: the host's home address. The address was hidden in listing responses until a booking was approved. That gate was a single string check on the booking row. Status equals approved, show the address. No check that the caller was the renter on that booking, or the host of that listing.

Combine that with the mutation problem above and the exploit writes itself. An anonymous caller creates a booking against any listing, flips its status to approved with a second request, then reads the listing and pulls the host's home address out of the response. Repeat in a loop and you have every host's home address in the database.

The product's README marketed address privacy as the headline guarantee.

Why this is so common in AI-built code

AI tools build exactly the schema they're given; if the route accepts a renterId, it uses it, because identity as a property of the session is the concept the prompt never stated.

AI coding tools and fast freelancers are very good at producing routes that work. They take the inputs the route needs, validate the shape, write the row, return the response. If the input shape includes a renterId, the route uses the renterId. The tool isn't lying about what it built. It's building exactly what the schema describes.

The missing concept is that the actor's identity is not an input. It is a property of the session, decided on the server, and the body should never get a vote. That distinction is obvious once you've seen it abused. It is invisible in a demo with one user, because in a demo the user clicking around always is the renterId they're sending.

The demo exercises the happy path. The happy path never asks who you are, because there's only ever been one of you.

What the fix actually is

Put auth in front of every mutation, read identity only from the verified session, drop user-id fields from request schemas, and check ownership before every write.

None of this is hard to fix. It is tedious, and the tedium is the point.

  1. Every mutation route gets an auth middleware in front of it, no exceptions.
  2. The actor's identity is read from the verified session, never from the body.
  3. The request schema loses its renterId and hostId fields entirely, because the server is going to set them itself.
  4. On update and delete, you load the row first, join to its parent if you need to, and compare the owner id against the session's user id.
  5. On a mismatch, return 404 rather than 403, so you don't tell an attacker which resource ids exist.

For the privacy gate, authorize first and redact second. The address is visible if and only if status equals approved AND the caller is the renter on that booking or the host of that listing. Two conditions, not one. Never trust a single status flag for a disclosure decision.

The pattern to take away

For every endpoint that writes a row, ask where the actor's identity comes from; if the answer is the request body or anything the client controls, that endpoint is open.

If you've shipped a marketplace, a multi-tenant SaaS, or anything where users hold data that other users shouldn't see, there's one question worth asking your codebase: for every endpoint that creates, updates, or deletes a row, where does the actor's identity come from? If the answer is the request body, or a value the frontend computed, or a header anyone can set, that endpoint is open.

That read is most of what an audit is for, and one of the first things a security team checks before your first enterprise customer. It is also the kind of finding that doesn't show up in any scanner, because nothing is broken in the technical sense. Every test passes. Every request returns 200. The code does exactly what it was asked to do. It just never got asked the right question.

Frequently Asked Questions

What is broken access control (IDOR)?

It is when an API lets a user act on or read data they don't own. In the marketplace described here, endpoints accepted the caller's identity (renterId, hostId) from the request body instead of the verified session, so anyone could create, approve, and read records as someone else.

Why don't security scanners catch this kind of bug?

Because nothing is broken in the technical sense. Every test passes and every request returns 200. The code does exactly what it was asked to do; it just never authorizes the caller against the resource. Catching it requires reading the code with an attacker's question in mind.

Should an unauthorized API request return 403 or 404?

Return 404 rather than 403 on an ownership mismatch, so you don't tell an attacker which resource ids exist.

Related Services

Need help with what you just read? These services are directly relevant.

Let's Talk

Ready to Build, Rescue, or Scale Your Product?

Tell us about your project. If it's a good fit, we'll schedule a strategy session.

Let's Talk

We respond within 4 hours during business hours. No obligation.