angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Скрытие первичных ключей базы данных UUID вашего веб-приложения

Не позволяйте другим людям играть с данными, которые выдает ваше веб-приложение.

29 марта 2024
post main image
https://unsplash.com/@davidkristianto

Создавая веб-приложение, вы должны быть очень осторожны, чтобы не выдать слишком много информации. Если вы используете (автоинкремент) Integer IDs в своей базе данных, то, вероятно, вы уже раскрываете слишком много информации. Некоторые примеры. Integer user_id позволяет легко догадаться, сколько новых регистраций вы получаете каждый день. С помощью Integer order_id легко угадать, сколько заказов вы получаете каждый день.

Кроме того, злоумышленники могут попытаться уменьшить или увеличить эти значения в URLs или формах. Если у вас нет соответствующей логики, то они могут увидеть ранее зарегистрированные user, предыдущие заказы.

Многие из этих проблем исчезают при использовании UUID4s в качестве первичных ключей. Это делает невозможным угадывание предыдущих значений. Но вам все равно нужно защищаться от перебора.

Но даже UUID4s мы иногда не хотим раскрывать. Существует множество методов скрыть свой IDs, здесь я представляю еще один. Предполагается, что вы уже используете UUID4s в качестве первичных ключей. Обратите внимание, что здесь нет действительно простых решений.

Как всегда, я делаю это на Ubuntu 22.04.

Как это работает

Предполагается, что вы уже используете UUID4s для первичных ключей (и внешних ключей).

Кодирование

Перед отправкой данных с сервера на клиент мы кодируем IDs в данных:

  • Мы заменяем все значения первичного ключа UUID4s , 'from_ids', на новые значения UUID4s , 'to_ids'.
  • Этот оригинальный и новый UUIDs хранятся во вновь созданном 'from_to_record'.
  • Этот 'from_to_record' записывается в базу данных.
from_to_record = {
    'id': <uuid4>,
    'user_account_id': <user_account_id>,
    'created_on': datetime.datetime.utcnow(),
    'to_id_from_ids': {
        <to_id1>: <from_id1>,
        <to_id2>: <from_id2>,
        ...
    },
}

Примечание(и):

  • При каждом кодировании создается новый 'from_to_record'.
  • 'from_to_records' никогда не изменяются, они создаются один раз и остаются действительными до истечения срока действия.
  • Мы храним не 'from_id_to_ids', а 'to_id_from_ids'. Причина в том, что мы используем 'from_to_records' только для поиска 'from_id' из 'to_id'.
  • Здесь мы показываем один столбец 'from_to_record' с типом столбца JSONB для хранения нескольких пар. Конечно, можно использовать и несколько записей.
  • 'user_account_id' связывает запись с конкретной user.

Декодирование

Когда мы получаем данные от клиента:

  • Сначала мы получаем непросроченные 'from_to_records' из базы данных, используя поля 'user_account_id' и 'created_on'.
  • Затем, используя эти записи, мы заменяем 'to_ids' в данных, полученных от клиента, на 'from_ids'.

Плюсы и минусы

Как уже говорилось, какой бы метод вы ни использовали, вам придется проделать дополнительную работу. Вот некоторые плюсы и минусы этого метода:

Плюсы:

  • Простая замена.
  • Почти никаких изменений на клиенте.
  • Генерация нового UUID4s является оптимизированной функцией.
  • Мы не трогаем оригинальные записи.
  • Легкие временные ограничения при использовании даты/времени создания.

Минусы:

  • Требуется база данных.
  • Не очень быстрая.

Пример: Приложение Flask с формой

Приведенный ниже очень ограниченный пример демонстрирует, как это работает. В примере мы можем перечислять и редактировать членов, не раскрывая фактические первичные ключи.

Мы используем Flask-Caching (FileSystemCache) в качестве базы данных для членов и для 'from_to_records'. Обычно вы используете реальную систему баз данных для участников и что-то вроде Redis для 'from_to_records'.

Создайте virtual environment , а затем:

pip install flask
pip install Flask-Caching

Существует три класса:

  • 'IdFromTo'
    Используется для получения и сохранения 'from_to_records' и для перевода 'from_ids' в 'to_ids', и наоборот.
  • 'Db'
    Запросы к базе данных
  • 'DbWrapper'
    Наши новые методы, обрабатывающие запросы 'from_ids' и 'to_ids' .

Дерево проекта:

.
├── project
│   ├── app
│   │   └── factory.py
│   └── run.py

Вот два файла:

# run.py
from app.factory import create_app

host = '127.0.0.1'
port = 5050

app = create_app()
app.config['SERVER_NAME'] = host + ':' + str(port)

if __name__ == '__main__':
    app.run(
        host=host,
        port=port,
        use_debugger=True,
        use_reloader=True,
    )
# factory.py
import datetime
import logging
import uuid
import os
import sys
from flask import current_app, Flask, redirect, render_template, request, url_for
from flask_caching import Cache

cache = Cache()

logging.basicConfig(
    format='%(asctime)s %(levelname)8s [%(filename)-15s%(funcName)15s():%(lineno)03s] %(message)s',
    level=logging.DEBUG,
)
logger = logging.getLogger()

# use string uuid
def get_uuid4():
    return str(uuid.uuid4())

class IdFromTo:
    def __init__(self, user_account_id):
        self.user_account_id = user_account_id
        self.expire_seconds = 30
        self.expired_on = datetime.datetime.utcnow() - datetime.timedelta(seconds=self.expire_seconds)
        self.from_to_records_loaded = False
        self.from_to_records = []
        self.from_ids_to_ids = {}

    def load_from_to_records(self):
        # filter here, we do not have a real database
        for from_to_record in cache.get('from_to_records') or []:
            if from_to_record['created_on'] < self.expired_on:
                logger.debug(f'expired, skipping from_to_record = {from_to_record} ...')
                continue
            if from_to_record['user_account_id'] != self.user_account_id:
                logger.debug(f'not a dataset of me, skipping from_to_record = {from_to_record} ...')
                continue
            self.from_to_records.append(from_to_record)
        from_to_records_len = len(self.from_to_records)
        logger.debug(f'from_to_records_len = {from_to_records_len}')

    # get from_id: match with previously saved from_to_records
    def get_from_id(self, to_id):
        if not self.from_to_records_loaded:
            self.load_from_to_records()
            self.from_to_records_loaded = True
        for from_to_record in self.from_to_records:
            to_id_from_ids = from_to_record['to_id_from_ids']
            if to_id in to_id_from_ids:
                return to_id_from_ids[to_id]
        logger.debug(f'not found in to_id_from_ids, to_id = {to_id}')
        return None

    # get to_id: create new/append to from_ids_to_ids
    def get_to_id(self, from_id):
        from_id = str(from_id)
        if from_id in self.from_ids_to_ids:
            # already created 
            logger.debug(f'use already created to_id_for from_id = {from_id}')
            return self.from_ids_to_ids[from_id]
        logger.debug(f'create new to_id_for from_id = {from_id}')
        to_id = get_uuid4()
        self.from_ids_to_ids[from_id] = to_id
        return to_id

    def save(self):
        # load, append, overwrite
        from_to_records = cache.get('from_to_records') or []
        # swap
        from_ids_to_ids_len = len(self.from_ids_to_ids)
        if from_ids_to_ids_len == 0:
            return
        to_id_from_ids = {}
        for from_id, to_id in self.from_ids_to_ids.items():
            to_id_from_ids[to_id] = from_id
        from_to_record = {
            'id': get_uuid4(),
            'user_account_id': self.user_account_id,
            'created_on': datetime.datetime.utcnow(),
            'to_id_from_ids': to_id_from_ids,
        }
        from_to_records.append(from_to_record)
        cache.set('from_to_records', from_to_records)

class Db:
    def __init__(self):
        # initial members
        self.members = [{
            'id': 'b5ff1840-38a8-44cc-8f54-730dcf0b1358',
            'name': 'John',
        },{
            'id': '27e14ff0-7620-4e17-9fa7-f491bab22c8a',
            'name': 'Jane',
        }]

    def get_members(self):
        members = cache.get('db_members')
        if members is None:
            # first time only
            cache.set('db_members', self.members)
        return cache.get('db_members')

    def get_member(self, member_id):
        for member in cache.get('db_members') or []:
            if member['id'] == member_id:
                return member
        return None

    def update_member(self, member_id, name):
        members = cache.get('db_members')
        for member in members:
            if member['id'] == member_id:
                member['name'] = name
                cache.set('db_members', members)
                return member
        return None

class DbWrapper:
    def __init__(self, user_account_id):
        self.user_account_id = user_account_id
        self.db = Db()

    def get_members(self):
        id_from_to = IdFromTo(self.user_account_id)
        members = []
        for member in self.db.get_members() or []:
            to_id = id_from_to.get_to_id(member['id'])
            members.append({
                'id': to_id,
                'name': member['name']
            })
        id_from_to.save()
        return members

    def get_member(self, to_id):
        id_from_to = IdFromTo(self.user_account_id)
        from_id = id_from_to.get_from_id(to_id)
        member = self.db.get_member(from_id)
        if member is None:
            return None
        return {
            'id': to_id,
            'name': member['name'],
        }

    def update_member(self, to_id, name):
        # do not swap change to_id
        id_from_to = IdFromTo(self.user_account_id)
        from_id = id_from_to.get_from_id(to_id)
        member = self.db.update_member(from_id, name)
        return {
            'id': to_id,
            'name': member['name'],
        }
    
def create_app():
    app = Flask(__name__, instance_relative_config=True)

    app.config.update({
        'CACHE_TYPE': 'FileSystemCache',
        'CACHE_DEFAULT_TIMEOUT': 3600,
        'CACHE_DIR': '.'
    })
    cache.init_app(app)

    user_account_id = '47742ae4-67bd-4164-b044-e893344c861c'

    db = DbWrapper(user_account_id)

    @app.route('/')
    def home():
        return redirect(url_for('members'))

    @app.route('/members', methods=['GET', 'POST'])
    def members():
        members = db.get_members()
        page_data_lines = ['Members:']
        for member in members:
            member_id = member['id']
            member_name = member['name']
            member_edit_url = url_for('member_edit', member_id=member_id)
            page_data_lines.append(f'<a href="{member_edit_url}">{member_name}</a> ({member_id})')
        return f"""{'<br>'.join(page_data_lines)}"""

    @app.route('/member/edit/<member_id>', methods=['GET', 'POST'])
    def member_edit(member_id):
        member = db.get_member(member_id)
        members_url = url_for('members')
        if member is None:
            return f"""Error: Expired?
                <a href="{members_url}">Start again</a>"""
        name = member['name']
        logger.debug(f'member = {member}')
        error = ''
        if request.method == 'POST':
            name = request.form.get('name').strip()
            if len(name) > 0:
                member = db.update_member(member_id, name)
                return redirect(url_for('members'))
            error = 'Error: enter a name'
        members_url = url_for('members')
        return f"""
            <a href="{members_url}">Members</a><br><br>
            Edit member:<br>
            <form method="post">
            <input type="text" name="name" value="{name}"><br>
            <input type="submit" name="button" value="Update">
            </form>
            {error}
            """

    return app

Чтобы запустить, перейдите в каталог проекта и введите:

python run.py

Затем направьте браузер на:

127.0.0.1:5050

Резюме .

Существует множество способов скрыть базу данных IDs. Представленное решение относится к базе данных, использующей первичные ключи UUID4 .

Перед отправкой данных клиенту первичные значения UUID4s заменяются на новые UUID4s, а исходные и заменяющие значения сохраняются. Когда данные поступают от клиента, мы загружаем исходные и заменяющие значения и производим замену. Если мы ждем слишком долго, значения заканчиваются, и нам приходится начинать все сначала.

В клиенте требуются лишь минимальные изменения.

Ссылки / кредиты

Hiding, obfuscating or encrypting database IDs
https://bhch.github.io/posts/2021/07/hiding-obfuscating-or-encrypting-database-ids

Подробнее

Flask Security

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.