in Flask, Software as a Service

Implement Short URL Tags in Flask Using Hashids

One issue you need to watch for in your SaaS application is using auto-incrementing IDs in your application’s URLs.

Good: http://myapp.com/invoice/bXW0y
Bad: http://myapp.com/invoice/9

In this article I will show you how to implement the first style of URL, matching the text string to an integer ID in your database.

Why is this an issue?

Your users likely share the same table for records, but can only see what they have access to. When they navigate to a URL like http://myapp.com/invoice/9, and later create an invoice and see the ID is 10, they know that the next invoice in your system is 11.

However, let’s say the invoice with ID 11 belongs to another user. To look up that invoice you need the invoice ID number and the user ID that is stored in a session variable. By knowing the invoice number is 11, a potential attacker already has one half of the equation without doing ANY work.

Auto-incrementing IDs in URLs also tell users information about your underlying system, like how many invoices have been created and how many users have signed up. In the scenario above, I would know that only 9 invoices have been created in the SaaS app and possibly question how mature the software is.

IDs in URLs can sometimes be ok in very public web sites where the next ID is an article or readily available piece of content. But I contend that in every SaaS application they are a bad idea.

Ways to Fix It

I researched several ways to work around this problem and narrowed it down to three possible solutions:

  • Stop passing IDs in URLs and use sessions or some other method
  • Generate a UUID in place of your primary key
  • Use hashids, a library for converting integers to short string IDs

The first option is not reasonable because it creates usability issues. Imagine your user Bob wants to email Gary a link to an invoice. Without an ID in the URL this is not possible. Bob would need to tell Gary how to browse to the right record in your system. You would also lose the ability to bookmark records and come back to them later.

The second option is very promising. UUIDs are unique and easy to generate. However, they are very long. Example: 123e4567-e89b-12d3-a456-426655440000. To implement UUIDs I would need to store them as the primary key or in a surrogate field called ‘slug’. I would have to shorten them to a reasonable length using base64 or some other method.

The last option, hashids, was my preferred option and fit the bill perfectly.

Implementing Hashids

Hashids work by encoding and decoding your ID, like so:

The result is a short string like ‘yds’ that is suitable for a URL.

One decision you need to make is whether to save your hashids in your database, or encode and decode them on the fly. I considered saving my hashids in a field called ‘slug’. However, after researching and reviewing a response from the creator of hashids, I opted to encode on the fly. The primary reason for this is to ensure speed when looking up records via simple integers.

Here is my final implementation in python Flask:

First, install the package with $ pip install hashids

I created two utility functions within my utils.py file. My app’s secret_key acts as the salt. Note: I’m using an application factory which is why I’m calling Hashid() in both functions.

We need to call the create_hashid function in our jinja2 templates. To do this, set a global environment variable like this:

In my template I have a row of items, with links generated like this:

Finally, our views.py file takes the link and displays the provided invoice:

The end result is our URLs no longer look like /invoice/32, but rather invoice/bXW0y. Nothing can be gained by looking at the letters. Everything is working perfectly.

Hope this was helpful!

Write a Comment

Comment