DEV Community

Cover image for Do not expose database ids in your URLs
Khalyomede
Khalyomede

Posted on • Updated on

Do not expose database ids in your URLs

When developing web apps, we often rely on fetching informations from the database. Frameworks offer a way to make this easy thanks to ORMs.

Most of the time, the ORM will find your model using the primary key as a reliable identifier. On the vast majority, primary keys are auto incremented integers.

Your URLs then look like this:

https://example.com/cart/12
https://example.com/user/15/post/41
...
Enter fullscreen mode Exit fullscreen mode

Providing an incorrect or faulty authorization layer can create data leaks: users become able to navigate from data to data, which is something we would not want to allow if it is about sensitive data like users personal info.

Obfuscating the identifier

An easy way to mitigate this security breach is to use a key that is:

  • Hard to predict
  • Random enough to be able to create a lot of items while keeping the unicity between them
  • Easy to generate from your code and the database
  • Checkable (we can know by analyzing its integrity if it is valid or not)

Thanks for us, UUIDs are a very good candidate for it. It checks all the points above, and are very easy to use thanks to a wide range of package ready for use.

Your URLS now become harder to predict, which mitigate any developers mistake regarding authorization policies in your app:

https://example.com/cart/f6e4208f-5df4-466e-9225-01f296e2a09c
https://example.com/user/b1b44b12-34bc-4ed7-a666-9657b8b8c31b/post/e530d034-42f7-467b-91e3-1cc9313312eb
Enter fullscreen mode Exit fullscreen mode

Example on a Laravel app

In order to practice developing a Laravel package, and using my first ever Github Workflow, I created a package to make this job a breeze.

Here is how you can use khalyomede/laravel-eloquent-uuid-slug in your app now.

Install the package

First, head in your console, and type this command:

composer require khalyomede/laravel-eloquent-uuid-slug
Enter fullscreen mode Exit fullscreen mode

Add your slug column to your migration

Then, go to the migration of the model of your choice, or create a new one if it has already been installed, and add the slug column.

namespace Database\Migrations;

use App\Models\Cart;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

final class AddSlugColumnToCartTable extends Migration {
  public function up(): void
  {
    Schema::table('carts', function (Blueprint $table): void {
      Cart::addSlugColumn($table);
    });
  }

  public function down(): void
  {
    Schema::table('carts', function (Blueprint $table): void {
      Cart::dropSlugColumn($table); // available soon in v0.2.0
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Add the trait to your model

This is the last step, which will help configure how your model is retreived in your routes using Route model binding.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Khalyomede\EloquentUuidSlug\Sluggable;

final class Cart extends Model
{
  use Sluggable;
}
Enter fullscreen mode Exit fullscreen mode

Use it in your controller

Now you are ready to take advantage of the package. The great thing with it is that your code does not change! You can keep using the route() method like you are used to.

// routes/web.php

use App\Models\Cart;
use Illuminate\Http\RedirectResponse
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\View\View;

Route::get("cart/{cart}", function (Cart $cart): View {
  return view("cart.show", [
    "cart" => $cart,
    "saveCartRoute" => route("cart.store", $cart),
  ]);
})->name("cart.show");

Route::post("cart/{cart}", function (Request $request, Cart $cart): RedirectResponse {
  $cart->update($request->only(["name"]));

  return redirect()->route("cart.show", $cart);
})->name("cart.store");
Enter fullscreen mode Exit fullscreen mode

And voilà! This package will no interfer with your existing logic as you can see. The only thing that changes is now your routes are not exposed.

route("cart.show", $cart); // https://example.com/cart/398e76a7-7c16-467c-93a8-04c06c6df703
Enter fullscreen mode Exit fullscreen mode

Conclusion

While this solution is not a magic way to resolve the initial problem of data leak, I find it is a very easy actionable mecanism to reduce the possibilities for malicious users to trick your system.

This does not prevent you to add an authorization or guard mecanism, like Laravel Policies for example. For example, if a user navigates to a cart that have not been created by him/her, should not be able to view it.

Other folks here already talked about this subject, so make sure to give it a go if you want to read more about using UUIDs:

Happy URL hardening!

Discussion (5)

Collapse
thumbone profile image
Bernd Wechner • Edited

The only drawback I see to UUIDs is that they are hellishly long, ugly and unfriendly. Shorter URLs are a strong preference and what lead, many of us to use the simply integer primary key.

That said, as far as slugs (when introducing an esoteric term like 'slug' I'd always counsel linking to a useful explanation) go, many sites successfully use words ... and as we all know by now three random words are as secure as long random strings but far more friendly, and as such many slugs are in fact constructed from titles and other related sources reducing to something like a word or two or three - separated.

Collapse
clovis1122 profile image
José Clovis Ramírez de la Rosa

I think UUIDs are great when you need to create objects that may live in multiple databases as it will uniquely identify the object regardless of where it was created.

But it is also worth noting that UUIDs are more inefficient in terms of representation which has a cost in both memory usage and performance. This cost is non-trivial for larger applications. One interesting read is percona.com/blog/2019/11/22/uuids-....

Sure it is bad that an attacker can predict your URLs, but you can fix the problem by implementing a good authentication layer. In comparison, there isn't an easy way to fix the performance penalties of using UUIDs...

Collapse
aminnairi profile image
Amin

I guess, a good fence on your property does not prevent you from having a hardened door for your house in order to mitigate possible failure in the fence, and I believe this is what is being explained here.

You should not only rely on one utility like Laravel policies because it might fail because of a human mistake or a script failure (security breach). UUIDs are an easy way of mitigating any leak in your policies until you find the root of the problem (or a patch is being published for the package used for managing policies by the maintainer of Laravel Policies).

Security has always a cost. It should not be compromised for the sake of performance or memory usage (even if this is interesting to know that this has a greater impact than using integers as primary keys).

Also, the article you linked talked about UUID as primary keys, whereas Khalyomede's solution is all about an additional column. And if you look a little closer, your article is describing in the end the exact solution used by this package (Mapping UUIDs to Integers).

Collapse
clovis1122 profile image
José Clovis Ramírez de la Rosa • Edited

Best solution from a security perspective is and will always be to have a working authentication layer. Obsfuscating (by using UUIDs instead of IDs) will never be a replacement to that. Attackers can adquire the identifier in any other API endpoint that you have exposed.

If you have a working authentication layer, you definitively should not pay the performance costs of using UUIDs for the sake of obsfuscation.

If you're worried that in the future, your authentication layer may break because of human mistakes, you should create integration tests to ensure that the issue is detected before it reaches production.

Putting security aside, sometimes you do have to use UUIDs for different reasons, like uniqueness across different systems. Here is where tactics like mapping UUIDs to integer help.

Collapse
slav123 profile image
Slawomir Jasinski

There is a much easier way, without need to create aditional fields in DB. It's called Optimus ID transformation and it's based on Knuth's integer hash - generates integer id based on 3 parameters and it's reversible. Sample library for PHP is available here:

Optimus id transformation