Obfuscate Django Models Ids by Encoding Them As Non-Sequential Non-Predictable Strings - DEV Community
Obfuscate Django Models Ids by Encoding Them As Non-Sequential Non-Predictable Strings - DEV Community
Andrea Bertoloni
Posted on 15 jun 2020
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.
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 .
mysite/
|-- db.sqlite3
|-- ids_encoder
| |-- __init__.py
| `-- converters.py
|-- manage.py
|-- mysite
| |-- __init__.py
| |-- settings.py
| |-- urls.py
| `-- wsgi.py
`-- ...
class HashidsConverter():
regex = '[0-9a-zA-Z]+'
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
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
register_converter(converters.HashidsConverter, 'hashids')
urlpatterns = [
path('users/<hashids:user_id>/', test_user_id, name='test_user_id'),
]
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
$ 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).
HASHIDS = {
# SECURITY WARNING: keep the salt used in production secret!
'SALT': 'Nel mezzo del cammin di nostra vita',
'MIN_LENGTH': 11
}
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
hashids = Hashids(**PARAMS)
These are the encode/decode functions that we could later import anywhere in the
project.
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
$ ./manage.py runserver
$ curl localhost:8000/users/zpA1nRdJbwG/
1
class UsersSerializer(ModelSerializer):
class Meta:
model = get_user_model()
fields = ['id', 'email']
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).
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 .
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
Start Building
Andrea Bertoloni
JOINED
10 feb 2020
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
https://dev.to/ndrbrt/obfuscate-django-models-ids-by-encoding-them-as-non-sequential-non-predictable-strings-4pa 9/9