0% found this document useful (0 votes)
8 views

Obfuscate Django Models Ids by Encoding Them As Non-Sequential Non-Predictable Strings - DEV Community

Django tutorial

Uploaded by

TomDijkshoornn
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views

Obfuscate Django Models Ids by Encoding Them As Non-Sequential Non-Predictable Strings - DEV Community

Django tutorial

Uploaded by

TomDijkshoornn
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 9

25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

Andrea Bertoloni
Posted on 15 jun 2020

Obfuscate Django models ids by encoding them


as non-sequential non-predictable strings
#django #python #hashids

TL;DR
Replace urls like example.com/users/1/ with something like
example.com/users/zpA1nRdJbwG/ without changing the actual ids in your db.
(Also applicable to stuff other than urls, e.g. replace api responses like { "id": 1,
"email": "[email protected]" } with something like { "id": "zpA1nRdJbwG", "email":
"[email protected]" } )

Sometimes, whatever the reasons, you may want to change the ids of your models,
which usually are incremental integers, with some non-sequential non-predictable
codes instead.
So, instead of having - for example - an url like example.com/users/1/ , you would have
something like example.com/users/zpA1nRdJbwG/ .

A common solution for this is to use UUIDs as the primary keys for your models.
Similarly, you could generate some other [random] unique strings when you create an
object in the db.
The problem here is that we're going to lose all the benefits that come from using
incremental integers as primary keys.

So, a solution might be to encode/decode the ids in the application layer, without
touching the actual incremental numeric ids in the database.
To do so we're going to integrate Hashids, a library "that generates short, unique, non-
sequential ids from numbers", into Django.

Install Hashids with

$ pip install hashids


# or pipenv install hashids
# or poetry add hashids

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 1/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

URLs
The first step is to register a custom path converter, so that we can then define a url as
path('users/<hashids:user_id>/', ...) that will automatically pass the decoded integer
id in the related view.

Create a folder named ids_encoder at the same level as any other Django app, with two
empty files inside: __init__.py converters.py .

# inside your project root directory


$ mkdir ids_encoder
$ cd ids_encoder
$ touch __init__.py converters.py

You should end up with something like this

mysite/
|-- db.sqlite3
|-- ids_encoder
| |-- __init__.py
| `-- converters.py
|-- manage.py
|-- mysite
| |-- __init__.py
| |-- settings.py
| |-- urls.py
| `-- wsgi.py
`-- ...

In ids_encoder/converters.py , paste this code

class HashidsConverter():
regex = '[0-9a-zA-Z]+'

def to_python(self, value: str) -> int:


hashids = Hashids()
decoded_values = hashids.decode(value)
# output of hashids.decode is always a tuple
if len(decoded_values) != 1:
raise ValueError
return decoded_values[0]

def to_url(self, value: int) -> str:


hashids = Hashids()
return hashids.encode(value)

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 2/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

This class has the shape required for a custom path converter:

a regex (as a string) that matches the parameter passed in the url
a method to_python which converts the matched string into the type that should be
passed to the view function
a method to_url which converts the Python type into a string to be used in the URL

We now need to register the custom converter, so go in mysite/urls.py

from django.urls import register_converter

from ids_encoder import converters

register_converter(converters.HashidsConverter, 'hashids')

urlpatterns = []

To test that everything works, we just write a simple view that returns the exact params
the it receives (remind: the view itself receives the already decoded value).

In mysite/urls.py

from django.urls import path, register_converter


from django.http import HttpResponse

from ids_encoder import converters

register_converter(converters.HashidsConverter, 'hashids')

def test_user_id(request, user_id):


return HttpResponse(user_id)

urlpatterns = [
path('users/<hashids:user_id>/', test_user_id, name='test_user_id'),
]

We are now ready to test it.


Generate a hashid like this

$ ./manage.py shell -c 'from hashids import Hashids; print(Hashids().encode(1))'


jR

Now, run the development server and send a request

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 3/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

$ ./manage.py runserver

and from another shell

$ curl localhost:8000/users/jR/
1

If everything is correct you should see that the curl command returns 1, which is the
value we encoded in the previous command.

Ok, the custom path converter works just fine, yet the encoded values are still easily
guessable by anyone with just as little as Hashids().decode(jR) .
That is why Hashids - no big surprise - let us set a salt (also a min_length and a custom
alphabet).

So now we are going to set our custom values in settings.py and doing a bit of
refactoring, ending up with custom encode/decode utility functions to also being able
to encode/decode ids in other parts of our project (for example in a DRF serializer).

Salt, min_length and encode/decode utilities


In mysite/settings.py add this dict

HASHIDS = {
# SECURITY WARNING: keep the salt used in production secret!
'SALT': 'Nel mezzo del cammin di nostra vita',
'MIN_LENGTH': 11
}

Create a new file ids_encoder/utils.py and paste this code in it

from django.conf import settings

def get_params():
try:
HASHIDS = settings.HASHIDS
except:
HASHIDS = {}

salt = HASHIDS.get('SALT')
min_length = HASHIDS.get('MIN_LENGTH')
res = {}
if salt: res['salt'] = salt
if min_length: res['min_length'] = min_length

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 4/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

return res

def get_regex(params):
min_length = params.get('min_length')
if min_length is not None:
return f'[0-9a-zA-Z]{{{ min_length },}}'
return '[0-9a-zA-Z]+'

PARAMS = get_params()
REGEX = get_regex(PARAMS)

get_params reads our custom settings and convert that in dict that can then be passed
to the Hashids initilizer.
get_regex returns the appropriate regex according to settings (e.g. if no min length is
set the regex would be '[0-9a-zA-Z]+' , if min length is 11 the regex would be '[0-9a-
zA-Z]{11,}' )

Now in ids_encoder/__init__.py

from .utils import PARAMS


from hashids import Hashids

hashids = Hashids(**PARAMS)

def encode_id(_id: int) -> str:


return hashids.encode(_id)

def decode_id(_id: str) -> int:


decoded_values = hashids.decode(_id)
# output of hashids.decode is always a tuple
if len(decoded_values) != 1:
raise ValueError
return decoded_values[0]

These are the encode/decode functions that we could later import anywhere in the
project.

And finally, in ids_encoder/converters.py , replace the previous code with

from .utils import REGEX


from . import encode_id, decode_id

class HashidsConverter():

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 5/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

regex = REGEX

def to_python(self, value: str) -> int:


return decode_id(value)

def to_url(self, value: int) -> str:


return encode_id(value)

Ok, now everything's done. We can test it as we did before.


Generate a hashid like this (note, we are now using our own encode_id function to
generate the hashid)

$ ./manage.py shell -c 'from ids_encoder import encode_id; print(encode_id(1))'


zpA1nRdJbwG

Run the development server and send a request

$ ./manage.py runserver

and from another shell

$ curl localhost:8000/users/zpA1nRdJbwG/
1

Example within a DRF serializer


If you want to obfuscate the id field in the response of a Django REST Framework view,
I would define a serializer like this

from rest_framework.serializers import ModelSerializer


from django.contrib.auth import get_user_model

from ids_encoder import encode_id

class UsersSerializer(ModelSerializer):
class Meta:
model = get_user_model()
fields = ['id', 'email']

def to_representation(self, instance):


"""Convert id to hashid"""
res = super().to_representation(instance)

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 6/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

res['id'] = encode_id(res['id'])
return res

Note that you may also want to override the to_internal_value method of a DRF
serializer (refer to the DRF docs).

Top comments (1)

Stephen B • 23 aug 21

Huge thanks for writing this, exactly what I needed. It worked seamlessly with DRF, and
easy enough to also add a HyperlinkedIdentityField to a serializer to generate the URLs if
you want to avoid to_representation or to_internal_value .

Code of Conduct • Report abuse

Stellar Development Foundation PROMOTED

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 7/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

A brief tutorial on creating on setting up your local environment


for Rust development
Follow our step-by-step guide to develop smart contracts on Soroban.

Start Building

Andrea Bertoloni

JOINED
10 feb 2020

More from Andrea Bertoloni

Python: upload multiple files concurrently with aiohttp and show progress bars with tqdm
#python #upload #aiohttp #tqdm

DEV Community

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 8/9
25-07-2023 08:07 Obfuscate Django models ids by encoding them as non-sequential non-predictable strings - DEV Community

Python Best Practices: A Guide to Writing Clean and


Readable Code
In this blog post, let's dive into some of the best practices for writing clean and readable code in
Python.

Read full post

https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 9/9

You might also like