Related articles

No items found.
The French newsletter for Ruby on Rails developers. Find similar content for free every month in your inbox!
Register
Share:
Blog
>

🍪🆔 Why and how we switched to v7 UUIDs in our Rails apps

Consider the following controller:

#app/controllers/private_messages_controller.rb
class PrivateMessagesController < ApplicationController
  def show
    @private_message = PrivateMessage.find(params[:id])
  end
end

It has an obvious security flaw: any user of the platform can find
private messages that are not intended for him.

Also consider this URL:

/investments/38

It exposes sensitive information: an overview of the number of investments, especially if
the user has just created his own and was directed to InvestmentsController #show.

As for the first example, I doubt you're writing code that creates a
such security breach. You have taken the reflex to write:

#app/controllers/private_messages_controller.rb
class PrivateMessagesController < ApplicationController
  def show
    @private_message = current_user.private_messages.find(params[:id])
  end
end

However, we are not in control of everything Legacy Code of an application, and this kind of flaw can exist without our knowledge and in a less obvious way than in the example.

As for the second example, there is no easy change to apply to the controller
to not expose the sequential investment identifier.

UUIDs can fix both of these issues at the same time. Let's see how!

What are UUIDs?

UUID stands for Universally Unique Identifiers. Traditional sequential identifiers are
repeat from one table in the database to another, as well as from one application to another. There is a User with ID 1 and a PrivateMessage with ID 1. The same integer numbers that increase one by one are used in a lot of applications. On the contrary, UUIDs are unique identifiers, not only from one database table to another, but from one application to another. Each represents a throng entirely unique, which will never be repeated.

There are several UUID standards, but let's focus on the one established by the Internet
Engineering Task Force. This standard has nine versions, which use multiple strategies to produce a throng Universally unique 32-character hexadecimal.

What are these strategies?

I. Use combinations of hashed namespaces and unique
II. Use Timestamps sometimes up to 100 nanoseconds
III. Use random characters
IV. Use a combination of Timestamps and random characters

Here is an example:

019bff1a-a80f-7bad-b4da-ea545f04972c

which is implementing the latter strategy.

Of the nine versions of UUID, PostgreSQL only uses two: versions 4 and 7. La
UUID version 4 is a throng random available since PostgreSQL version 13. La
version 7 of the UUID, which combines a Timestamp and random characters has been available since PostgreSQL version 18.

The Timestamp of version 7 allows objects to be ordered by their UUID, which is not
not possible with the throng random from version 4. This is why version 7 is preferable to version 4 for our purposes.

The anatomy of UUID version 7

Let's take a closer look at UUID version 7. Our example UUID includes
32 hexadecimal characters, excluding hyphens:

019bff1a-a80f-7bad-b4da-ea545f04972c

Let's break down each of these characters:

TTTTTTTT-TTTT-7RRR-VRRR-VRRR-RRRRRRRRRRRRRR

T - Timestramp
R - Random
V - Variant

Let's first analyze the Timestamp, which takes up the first twelve characters of throng.
019beb65bca4 is an integer representing Unix time expressed in hexadecimal. We can convert it to a date object in the following way:

Time.at("019beb65bca4".to_i(16).fdiv(1000)
=>
2026-01-23 15:07:51

you_i (16) Convert hexadecimal to the number of milliseconds, which we then convert
in seconds.

After the Timestamp, there is a 7, which is invariable and represents the UUID version. Please
was a version 4 UUID, there would be a 4.

The other values are random except for V, which represents the UUID variant.
This variant makes it possible to distinguish the Internet Engineering Task Force UUID standard from other standards. The Internet Engineering Task Force uses four values to represent this variant: 8, 9, A, and B, since these values, expressed in binary, always start with 10.

In our example, it's a B:

019bff1a-a80f-7bad-B4da-ea545f04972c‍

That leaves us with 18 random hexadecimal characters. Without even taking into account the
Timestamp, the probability of finding these 18 characters and the variant is 16 characters
hexadecimals to the power of 18 times 4 variants.

This colossal number of possibilities makes the UUID unique, impossible to guess and therefore
secure.

How do I use UUIDs with a PostgreSQL database?

Now that you have a theoretical understanding of UUIDs, let's look at how
use them in a Ruby on Rails application with PostgreSQL.

UUIDs in PostgreSQL are uuid column types, just like identifiers
sequential have the bigint type.

Even though we prefer version 7 of the UUIDs, let's look at the implementation of these two versions.

To create a new table with a v4 UUID, simply do:

create_table :users, id: :uuid, default: -> { "gen_random_uuid()" } do |t|

With a v7 UUID

create_table :users, id: :uuid, default: -> { "uuid_generate_v7()" } do |t|‍

Foreign keys for both versions must specify the uuid type:

add_reference :private_messages, user, foreign_key: true, index: true, type: :uuid‍

Let's go back to our initial examples:

#app/controllers/private_messages_controller.rb
class PrivateMessagesController < ApplicationController
  def show
    @private_message = PrivateMessage.find(params[:id])
  end
end

Even though you can access any private message by typing in the username of the
message in the URL, it is impossible for a malicious user to guess the ID of a private message that they are not supposed to see.

Consider our URL again:

/investments/38

With a UUID, it becomes

/investments/019bff1a-a80f-7bad-b4da-ea545f04972c

and the number of investments is now hidden.

So, UUIDs, whether version 4 or 7, secure your application from the database and potentially obfuscate information sensitive.