Django + redis Issue a dedicated URL valid for 1 hour

environment

python:2.7 Django:1.6 pip install redis

Timing when you want to issue a dedicated URL

When it comes to dedicated pages, login + cookies are common with accounts, but there are times when you want to issue a dedicated URL for each user without logging in. For example, we do not issue an account to the user because it is common not to issue a login ID for emails at the time of initial registration or for recent smartphone games.

Possible attacks and countermeasures

I'm afraid of dictionary attacks and brute force attacks, so it should be okay if you take measures against it

  1. As a measure against dictionary attack, the URL character string must be a disposable character string that cannot be inferred from the user ID.
  2. Put a checksum in the disposable character string issued in 1 as a countermeasure against brute force attacks.

↓ It's probably faster to read the code

m.py


from uuid import uuid4

# 1.Issuance of disposable ID
_uuid4 = str(uuid4())
print 'uuid4:', _uuid4

# 2.Disposable ID checksum
uuid4_char_list = [ord(_char) for _char in _uuid4]
print 'uuid4_char_list:', uuid4_char_list
checksum = sum(uuid4_char_list)
print 'checksum:', checksum

# 3.URL issuance
print "http://hoge/page?token={}%20cs={}".format(_uuid4, checksum)

Execution result


> python m.py 
uuid4: 6da25bb0-5d9c-4c1e-becc-51e3d5078fe4
uuid4_char_list: [54, 100, 97, 50, 53, 98, 98, 48, 45, 53, 100, 57, 99, 45, 52, 99, 49, 101, 45, 98, 101, 99, 99, 45, 53, 49, 101, 51, 100, 53, 48, 55, 56, 102, 101, 52]
checksum: 2606
http://hoge/page?token=6da25bb0-5d9c-4c1e-becc-51e3d5078fe4%20cs=2606

If you review this mechanism from a security point of view ...

When reviewing, it is often said: "Isn't it dangerous to be analyzed as a checksum? 』
I will fix the logic by enduring the place where I want to say that there is no strange hacker who analyzes our depopulated system.

m.py


# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import binascii
from uuid import uuid4


def checksum_base64_crc(_str):
    """
Inverts the entered string, base64 encodes it, and returns a crc32 checksum
    :param _str: str
    :rtype : int
    """
    #Invert
    _str_r = _str[::-1]

    #Base64 encode and take crc32 checksum
    return binascii.crc32(binascii.b2a_base64(_str_r))


# 1.Issuance of disposable ID
_uuid4 = str(uuid4())
print 'uuid4:', _uuid4

# 2.Disposable ID checksum
checksum = checksum_base64_crc(_uuid4)
print 'checksum:', checksum

# 3.URL issuance
print "http://hoge/page?token={}%20cs={}".format(_uuid4, checksum)

Execution result 2


>python m.py 
uuid4: 6a1d87e0-0518-4aa0-a2ca-cced091f254b
checksum: -2147023629
http://hoge/page?token=6a1d87e0-0518-4aa0-a2ca-cced091f254b%20cs=-2147023629

Manage URLs issued by redis.

If too many tokens are issued, there is a fear of DB bloat. It is convenient to manage data that disappears over time and has no problem with redis. Since the data will disappear over time, it will be easier later if you keep the URL issued as a countermeasure against inquiries in the log.

Finished product

token_manager.py


# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import binascii
from redis import Redis
from uuid import uuid4


def checksum_base64_crc(_str):
    """
Inverts the entered string, base64 encodes it, and returns a crc32 checksum
    :param _str: str
    :rtype : int
    """
    #Invert
    _str_r = _str[::-1]

    #Base64 encode and take crc32 checksum
    return binascii.crc32(binascii.b2a_base64(_str_r))


class IncorrectCheckSumError(Exception):
    #Wrong checksum
    pass


class TokenExpiredError(Exception):
    #Token expired
    pass


class TokenRepository(object):
    """
Token for URL is stored and managed in Redis for a certain period of time
    """
    EXPIRE_SEC = 3600
    _KEY_BASE = "project_name/url/disposable/{}"
    _cli = None

    @property
    def client(self):
        """
        :rtype : Redis
        """
        if self._cli is None:
            self._cli = Redis(host='localhost', port=6379, db=10)
            return self._cli
        return self._cli

    def get_key(self, token):
        return self._KEY_BASE.format(str(token))

    def set(self, token, value):
        self.client.setex(self.get_key(token), value, self.EXPIRE_SEC)
        return

    def get(self, token):
        """
Return the associated value
        :param token:
        :return: str
        """
        value = self.client.get(self.get_key(token))
        return str(value)

    def exist(self, token):
        """
Check if the token exists in the repository
        :param token: unicode or string
        :rtype: bool
        """
        return bool(self.get(token))


class TokenManager(object):
    @classmethod
    def validate(cls, token, check_sum):
        """
Check if the token is correct
        :param token: unicode or str
        :param check_sum: int
        :rtype : bool
        """
        token = str(token)
        check_sum = int(check_sum)

        #Check if the token is correct with a checksum
        if checksum_base64_crc(token) != check_sum:
            raise IncorrectCheckSumError

        user_id = TokenRepository().get(token)
        return bool(user_id)

    @classmethod
    def generate(cls, user_id):
        """
Generate token and checksum
        :param user_id:
        :return: str, int
        """
        #Generate
        token = str(uuid4())

        #Generated token and user_Save id linked to redis
        TokenRepository().set(token, user_id)

        return token, checksum_base64_crc(token)

    @classmethod
    def get_user_id_from_token(cls, token, check_sum):
        """
from token to user_Subtract id
        :param token: str or unicode
        :param check_sum: int
        :rtype: str
        """
        token = str(token)
        if not cls.validate(token, check_sum):
            raise TokenExpiredError
        return TokenRepository().get(token)


# 1.Issue a disposable URL
url_base = "http://hogehoge.com/hoge?token={}&cs={}"
user_id = "1111222333"
_token, check_sum = TokenManager.generate(user_id)
url = url_base.format(_token, str(check_sum))
print url

# 2.Road test
assert TokenManager.get_user_id_from_token(_token, check_sum) == str(user_id)

View on Django side


# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django.http import HttpResponse
from django.views.generic import View


class ExclusiveView(View):
    def get(self, request, *args, **kwargs):
        # HTTP GET
        token = request.GET.get("token", None)
        check_sum = request.GET.get("cs", None)

        #from token to user_Subtract id
        error_message = None
        message = None
        try:
            user_id = TokenManager.get_user_id_from_token(token, check_sum)
            message = "Your user_id is{}is".format(user_id)
        except IncorrectCheckSumError:
            error_message = "Illegal parameter"
        except TokenExpiredError:
            error_message = "Page expired"
        if error_message:
            message = error_message

        return HttpResponse(message,
                            mimetype='text/html; charset=UTF-8',
                            *args, **kwargs)

Execution result

URL issuance


>python m.py 
http://hogehoge.com/hoge?token=e359b20e-4c60-48da-9294-2ea9fcca0a6c&cs=-2066385284

■ Access with a browser Normal system スクリーンショット 2015-10-15 15.20.04.png

■ Access with a browser Abnormal system スクリーンショット 2015-10-15 15.20.16.png

Recommended Posts

Django + redis Issue a dedicated URL valid for 1 hour
Create a model for your Django schedule
Commands for creating a new django project
Build a TOP screen for Django users
Issue a signed URL with AWS SQS
Write a short if-else for Django Template
Create a dashboard for Network devices with Django!