8 Comments

Why you shouldn't expose your incrementing IDs

By default, every Laravel model has an incrementing ID. Although this makes things very easy, there are some security reasons why you shouldn't be exposing this ID to your users. In this article, I'm going to explain why.

Security risk

If you use default incrementing IDs in your URLs, anyone can guess which one is the previous, and which one is the next. For example, you've got orders and you use this URL scheme:

http://www.example.org/orders/9

Anyone can know that the previous was 8 and the next will be 10. Of course, you don't want your users to know what others bought.

Although you should return a 403 error if an unauthorized user tries to visit http://www.example.org/orders/8, if you forget to do so you have a big security risk.

Resource harvesting

If you have images and corresponding records in your DB, you can have image names like those:

http://example.org/images/avatar34.jpg
http://example.org/images/avatar35.jpg

And anyone can visit http://example.org/images/avatar33.jpg and so on to get all your images. This practice is called resource harvesting and applies to models too but is a bigger problem with static files.

Others know how big your website is

If someone registers and visits their account page, they may end up with a URL like this one:

http://example.org/users/223

Now they know they are number 223. This may not seem like an issue at first, but your competition can create an account too and see how many users you have.

Of course, if you create a forum thread or something else with the same URL scheme, you have this problem too. For example:

http://example.org/threads/43
http://example.org/books/some-book/reviews/34

Solutions

Fortunately, there are plenty of solutions available for this problem. I've described two.

Slugs

You may use slugs to have a unique identifier that exposes nothing about your DB size.

A post about a specific subject
a-post-about-a-specific-subject

And it is what I do with my blog too. But for some models, a slug doesn't make much sense, such as for users or reviews.

And besides, it's a lot of work to get slugs working, especially if everyone can create a new model. For example:

  • What if two slugs are the same?
  • What if a title changes?

UUIDs

A UUID (Universally Unique Identifier) is kind of a normal ID, except no-one knows what the previous or next one is.

This is an example of a UUID:

e4a3e607-24b0-40e0-b56d-882304123506

And you'll get URLs like this one:

http://www.example.org/orders/065e9fb3-6bec-494c-9917-4ab8e71750d4
http://www.example.org/users/d3835dda-e08f-4ed5-baff-756d62a749b2
http://www.example.org/images/avatar-d54a923b-c5d4-4294-8033-a221a57ef361.jpg

If you would like to learn more about UUIDs, I've written a tutorial about how to use UUIDs in your URLs with Laravel.

Share this article:

Subscribe to my newsletter

Continue reading:

How to use UUIDs in URLs in Laravel

By default, every Laravel model has an incrementing ID. Although this makes things very easy, there are some security reasons why you shouldn't be exposing this...

Leave a comment

Comments (8)

    Phread's avatar
    Phread Reply
    4 months ago

    I looked into using UUID and decided that I would use the following approach as it was easier for me at the time. I believe I wold still use the same approach.

    For each of the incremental IDs I set the value to start at a random extremely high number for each of the non-reference tables. Additionally, each of the incremental ID starting value was different. It is possible that at some point in the future they may have similar/same values, but it really doesn't matter. Nobody would be able to determine how many of each Area exists, or they will believe the # is much higher than it really is. The non-reference tables are called Areas, of which I currently have ~26 (User, Banking, Checking, Insurance, Vehicle, etc). The incremental value is displayed in the URL, but it really does no good for any of the Users to change this value. Why?

    a) The value belongs to a specific User for as specific Area. The Controller qualifies on the ID AND the User ID. At not time is the User ID display within the URL, nor is it hidden on the form (hidden field).

    b) Each controller gets the User ID for the current User to be added to the update/delete action.

    c) The User is always taken to the Area's Dashboard upon clicking on the Save/Delete button.

    i) A message is display at the top of the Area's Dashboard notifying them of the successful action or if they had changed the ID, it will display that the entry no longer exists. d) The User ID is not persisted in memory and is retrieved for each each action (Display, Create, Edit, Delete). Nope, I do not want the User to know their User ID as it gives them no value AND it would/could legal issues for me.

    I have used the approach for a number of applications, number of interfaces, number of languages, and it has worked well for me. However, I am always looking for alternatives which are safer and better. I like the UUID approach, but it just seemed to add a level of complexity that was gong to cause me issues when/if I changed platforms.

    Thoughts ?

      Jeroen van Rensen's avatar
      Jeroen van Rensen
      4 months ago

      Hi Phread,

      Have you read my article about how to use UUIDs in Laravel? https://www.jeroenvanrensen.nl/blog/uuids-in-laravel

      I think it's quite easy, especially when you change Laravel's stubs. But if you don't think that's true, I'll be happy to hear what your opinion is.

      Thank you! Jeroen

    Kaylakaze's avatar
    Kaylakaze Reply
    4 months ago

    We had a similar issue at one company I was with. Because of how it was set up, we couldn't switch the existing system over to UUIDs. What we did instead was encrypt the ID. We used route-model binding on the encrypted ID endpoints to decrypt the IDs before loading our controllers. Then, when building our links, instead of calling $model->id, we'd call $model->obf_id (which would call a setObfIdAttribute method which returned the encrypted form of $model->id).

      Jeroen van Rensen's avatar
      Jeroen van Rensen
      4 months ago

      Hi Kaylakaze,

      Yes, that’s also a great approach. If I may ask: which encryption method do you use, because not all of them allow you to decrypt.

      Thanks! Jeroen

      Kaylakaze's avatar
      Kaylakaze
      4 months ago
      I think it's using AES_256_CBC. I didn't write the encryption code myself, and I think my coworker likely just pulled an encryption/decryption implementation from Stack Overflow.
      Jeroen van Rensen's avatar
      Jeroen van Rensen
      4 months ago

      Thanks!

    Vignesh's avatar
    Vignesh Reply
    4 months ago

    Or you could try http://hashids.org/. Using UUID as primary key with one to many relations results in higher DB size.

      Jeroen van Rensen's avatar
      Jeroen van Rensen
      4 months ago

      Hi Vignesh,

      Did you read this article?

      I don't recommend using a UUID as a primary key, only for your routes.

      Thank you! Jeroen