> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# What Your Incrementing Order IDs Tell Your Competitors
- URL: https://debugly.dev/id-or-and-what-incrementing-ids-leak/
- Published: 2026-09-20T07:30:00.000Z
- Updated: 2026-09-20T07:30:00.000Z
- Author: Rohit Bhadani
- Tags: Security, Ecommerce, Debugging

A competitor was quoting the store's monthly order volume in a podcast, and the source was not a leak. It was the order confirmation page. Place an order on the first of the month, place one on the last, subtract the IDs, and you have the month's order count to within noise. The store was publishing its volume to anyone willing to buy a product, and the IDs that made it possible looked like the most harmless field in the system.

Sequential identifiers carry two distinct costs that get conflated. They disclose business information, and they turn any authorisation mistake into a trivial, complete exploit. Both are worth naming separately, because the fixes differ.

This was an ecommerce platform with incrementing integer order and customer IDs, and the analysis below is the one an attacker does in an afternoon.

## The disclosure

A sequential ID is a counter, and a counter is information. Order IDs reveal order volume and its growth rate. Customer IDs reveal user count. Invoice IDs reveal billing activity. None of this requires a vulnerability. It requires two samples and subtraction, and the samples are cheap because the system hands them to anyone who completes a transaction, or in many cases to anyone who views a public profile.

The disclosure compounds with time. Two samples a month apart give the rate. Two samples a year apart give the trend. A competitor who places a small order quarterly is running a telemetry feed on your business, funded by your own checkout.

The standard defence, adding random offsets or starting at a large number, slows the naive version but does not remove it, because the sequence is still order preserving and the deltas still average to the volume. Only non sequential identifiers remove the signal.

## The attack multiplier

The second cost is the one that matters in a security review. Most authorisation bugs are of the form "the server fetches the object by the ID in the request without checking that the requester may see it". With sequential IDs, that bug is exploitable by incrementing: the attacker who can see order 1001 can read order 1000, 999, and every order back to one, in a simple loop, at the rate the API allows.

This is why IDOR, insecure direct object reference, findings are rated by the enumerability of the identifier. A random, unguessable identifier turns the same missing check into a much smaller finding, because the attacker cannot walk the space. The authorisation bug is still a bug and must be fixed, but the sequential ID converted it from a defect into a harvest.

So the identifier choice does not cause the vulnerability. It scales it. The fix that matters is the authorisation check, and the identifier choice is the difference between a bug and a breach, which is the argument for treating both as part of the same review, per [reviewing input validation at boundaries](https://debugly.dev/reviewing-input-validation-at-boundaries/).

## The fix that works

**Random, unguessable public identifiers.** The public face of the object should be a random value with enough entropy that walking the space is infeasible, a UUIDv7 or a random token, while the internal integer can remain for joins and storage if it must. The separation of internal and external identity is the durable fix, and it also frees migrations, because the public ID no longer mirrors the storage layout.

**Authorisation on every fetch by external ID.** The check that the requester may read the object is the vulnerability fix, and it must be present regardless of the identifier, because random IDs are defence in depth, not a permission system. The two fixes are complementary and reviewers should ask for both.

**Scope the enumeration surface.** Rate limiting and anomaly detection on object fetches catch the walking loop when it happens, per [the rate limit that counted IPs](https://debugly.dev/brute-force-and-the-rate-limit-that-missed/), and should count per account and per token, because the walk is exactly the pattern those units catch.

## The counterarguments, honestly

Sequential IDs have real benefits and I want to name them. They are sortable, they fit in small indexes, they are friendly in support conversations, and they communicate recency to humans. Internal sequential keys retain all of those benefits, which is why the fix keeps them internally and randomises only the external surface.

There is also the argument that volume disclosure is not sensitive because competitors can estimate it from other signals. That is true for some businesses and false for others, and it is a decision to make on purpose rather than to leak by default. The default should be silence.

## The rule

Treat sequential public identifiers as a disclosure and as an attack multiplier, and fix both halves: randomise the external ID so the space cannot be walked and the volume cannot be counted, and keep the authorisation check on every fetch so the identifier's randomness is depth, not the permission.

The internal integer can stay, doing its quiet join work, as long as it never crosses the boundary. The boundary is where the information lives, and the boundary is where the review belongs.

The same "the identifier is the vulnerability" dynamic appears in open redirects, where the predictable thing is the destination rather than the object, in [the login redirect parameter](https://debugly.dev/open-redirect-in-the-login-flow/).

## The migration that does not scare anyone

Moving an established system from sequential public IDs to random ones is a migration, and the safe shape is the expand and contract pattern from [reviewing database migrations](https://debugly.dev/reviewing-database-migrations/). Add the external identifier column and backfill it, serve both identifiers while clients migrate, accept either at the API for a window, then drop the sequential value from responses last. The internal integer never moves, so joins and indexes are untouched, and the public surface changes without a flag day. The order of the steps is the safety: the new identifier exists and is populated before anything stops reading the old one, so no client ever sees a gap, and the disclosure ends when the last response stops carrying the counter.