October 9, 2026
UUIDs vs Sequential IDs: Which One Actually Stops IDOR?
Making an ID harder to guess isnβt the same as protecting the data behind it.

By Haroon Shahid
5 min read
Imagine logging into a website, opening your invoice, and noticing this URL: https://example.com/api/invoices/1042
Out of curiosity, you change 1042 to 1043.
You hit Enter.
And there it is. An invoice belonging to somebody else.
Their name, billing address, purchase details, everything.
You've just encountered a classic example of Insecure Direct Object Reference (IDOR).
Now imagine the development team discovers the problem and replaces those predictable numbers with UUIDs.
Instead of /invoices/1042, the endpoint looks like this:
/invoices/550e8400-e29b-41d4-a716-446655440000
That's definitely harder to guess.
But here's the interesting question.
Did they fix the vulnerability, or did they just make it harder to exploit?
Those are two very different things.
First, what's the difference between these IDs?
A sequential ID is exactly what it sounds like.
Records are assigned numbers that typically increase as new records are created.
For example:
- User A:
1001 - User B:
1002 - User C:
1003
If an application exposes these IDs, figuring out which number comes next isn't particularly difficult.
A UUID, on the other hand, looks something like this:
550e8400-e29b-41d4-a716-446655440000
It's much longer and doesn't follow the simple counting pattern of a sequential ID.
But there's a detail worth knowing: not all UUIDs are equally unpredictable. Randomly generated UUIDv4 values are extremely difficult to guess, whereas other UUID versions may contain timestamps or structured information.
For this comparison, we're talking about properly generated random UUIDs.
And yes, they make guessing valid identifiers much harder.
That's a real security benefit.
Just not the benefit some people think it is.
Let's look at how IDOR actually happens
Suppose we have a web application with two customers.
Alice owns invoice 1042.
Bob owns invoice 1043.
Alice signs into her account and opens her invoice. Her browser sends:
GET /api/invoices/1042
The server checks the database, finds invoice 1042, and returns it.
Everything works normally.
But what happens if Alice sends this request instead?
GET /api/invoices/1043
If the server returns Bob's private invoice, we've got a problem.
And the reason isn't that the identifier was easy to guess.
The problem is that the server failed to check whether Alice was allowed to access Bob's invoice.
Think about that for a moment.
The application knows who Alice is. She's logged in.
It knows which invoice she requested.
But somewhere between receiving the request and returning the data, it forgot to ask the most important question:
Does this invoice actually belong to Alice, or is she otherwise permitted to view it?
That's where the vulnerability lives.
Now replace both IDs with UUIDs
Let's give Alice and Bob randomly generated invoice identifiers instead.
Alice's invoice: 8a1c7d29-6f83-4e12-a3b4-9c2d5e7f1081
Bob's invoice: c72e91b4-3d6a-4f80-9b12-e5d76a2c4130
Alice can't simply increase the last digit and expect to find Bob's invoice.
Good.
But suppose she somehow learns Bob's UUID through a legitimate application interaction, such as a shared team view where identifiers are visible.
She sends: GET /api/invoices/c72e91b4-3d6a-4f80-9b12-e5d76a2c4130
And the server returns Bob's private invoice, even though Alice has no permission to access it.
What changed?
Almost nothing.
The identifiers are harder to guess, but the authorization flaw is still there.
A UUID tells the application which resource you're requesting. It doesn't automatically tell the application whether you're allowed to access it.
That distinction is the entire point.
But how would someone obtain another user's UUID?
This is usually where the discussion gets interesting.
A common defense of UUIDs is:
"If nobody can guess the UUID, how could they exploit the IDOR?"
Fair question.
The mistake is assuming that guessing is the only way to obtain an identifier.
Consider applications where users collaborate, share documents, belong to organizations, or interact with one another.
Identifiers can legitimately appear in API responses, shared document references, project dashboards, or other parts of a workflow.
An identifier might be visible to a user without that user having permission to perform every action associated with the referenced object.
For example, being allowed to see that an invoice exists doesn't necessarily mean you're allowed to download it, change its payment information, or delete it.
And this matters enormously in multi-user applications.
A UUID can prevent someone from easily discovering thousands of resources through enumeration.
It cannot repair an endpoint that accepts a known identifier without checking the caller's permissions.
How I'd test this in a lab
Here's a simple approach that doesn't require an expensive scanner or thousands of requests.
All you need is a web application you're authorized to test, two accounts, and an HTTP interception tool such as Caido.
Create two accounts: User A and User B.
Have each account create a private resource, such as an invoice, document, or support ticket.
While logged in as User B, capture the request used to access B's resource.
For example: GET /api/invoices/c72e91b4-3d6a-4f80-9b12-e5d76a2c4130
Now comes the important part.
Send the same request using User A's authenticated session, without changing the resource identifier.
Pay attention to the response.
If the invoice is private and User A receives User B's data, you've demonstrated an authorization failure.
If the application denies the request, that's what you'd expect from a properly enforced access policy.
And don't limit your thinking to reading data.
An application might correctly protect GET requests while forgetting authorization checks on PATCH, PUT, DELETE, or export endpoints.
Test those operations safely within the permissions of your lab or bug bounty program.
One more thing: an HTTP 200 OK alone doesn't prove an IDOR. You need to establish that the requesting account genuinely wasn't authorized to access the returned resource.
That's the difference between noticing unusual behavior and proving a vulnerability.
So are sequential IDs insecure?
Not inherently.
This is another misconception worth clearing up.
An application using sequential IDs can enforce excellent access controls.
Imagine Alice sends: GET /api/invoices/1043
The server identifies Alice from her authenticated session, checks her permissions, and discovers that invoice 1043 belongs to Bob.
Access denied.
Even though Bob's ID was easy to predict, his information remained protected.
Now compare that with an application using random UUIDs but performing no object-level permission checks.
One application has predictable identifiers and proper authorization.
The other has unpredictable identifiers and broken authorization.
Which application actually protects its users?
The first one.
Of course, that doesn't mean sequential IDs are always preferable. They can make enumeration easier and may reveal information about record ordering or volume.
Random identifiers can reduce those risks.
The point is that identifier design and authorization solve different problems.
What should developers actually fix?
The most important fix happens on the server.
Whenever someone requests a protected resource, the application needs to verify whether that person is authorized to perform the requested action.
Consider this simplified database query:
SELECT * FROM invoices WHERE id = :invoice_id;
By itself, this retrieves whichever invoice matches the supplied ID. If the application has no additional permission check, that's dangerous.
For an application where only the owner may view an invoice, a safer query could look like this:
SELECT * FROM invoices WHERE id = :invoice_id AND owner_id = :authenticated_user_id;
Now the query considers both the requested invoice and the authenticated user's identity.
Real applications may have more complicated rules involving teams, organizations, administrators, or explicitly shared resources.
The principle remains the same.
The server must enforce the actual permission rules, not trust an identifier supplied by the client.
UUIDs can still be used as an additional protection against enumeration.
They just shouldn't be mistaken for the authorization system itself.
One nuance: some applications deliberately use secret, high-entropy sharing links that grant access to whoever possesses the token. That's a different security model, and it needs carefully designed permissions and token handling. It doesn't mean ordinary private resources should rely on hidden IDs for protection.
Which one wins?
Here's the answer.
If you're asking which identifier is harder to guess, a properly generated random UUID wins easily.
If you're asking which one prevents IDOR, neither does by itself.
A sequential ID isn't automatically vulnerable.
A UUID isn't automatically secure.
The deciding factor is whether the application checks permissions before giving someone access to a resource.
So the next time you see an application using long, complicated identifiers, don't immediately assume its authorization is solid.
Ask yourself something else:
What happens if I already know another user's identifier?
That question is far more useful to a pentester than counting how many characters are in an ID.