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

Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web

No facilite que otros jueguen con los datos que expone su aplicación web.

29 marzo 2024
post main image
https://unsplash.com/@davidkristianto

Cuando creas una aplicación web, debes tener mucho cuidado de no exponer demasiada información. Si utiliza (autoincremento) Integer IDs en su base de datos, probablemente ya esté exponiendo demasiada información. Algunos ejemplos. Un Integer user_id hace que sea fácil adivinar cuántos registros nuevos recibes cada día. Un Integer order_id hace que sea fácil adivinar cuántos pedidos recibes cada día.

Además, los visitantes pueden intentar disminuir o aumentar estos valores en URLs o formularios. Si usted no tiene la lógica apropiada presente, entonces ellos pueden ser capaces de ver users registrados previamente, pedidos anteriores.

Muchos de estos problemas desaparecen al utilizar UUID4s como claves primarias. Esto hace que sea imposible adivinar los valores anteriores. Sin embargo, hay que protegerse contra la fuerza bruta.

Pero incluso UUID4s a veces no queremos exponer. Hay muchos métodos para ocultar su IDs, aquí presento otro. El supuesto es que usted ya utiliza UUID4s como claves primarias. Tenga en cuenta que no hay soluciones realmente fáciles aquí.

Como siempre hago esto en Ubuntu 22.04.

Cómo funciona

La suposición es que ya utiliza UUID4s para claves primarias (y claves externas).

Codificación

Antes de enviar los datos del servidor al cliente, codificamos IDs en los datos:

  • Sustituimos todos los valores de la clave primaria UUID4s , el 'from_ids', por nuevos valores UUID4s , el 'to_ids'.
  • Este UUIDs original y el nuevo se almacenan en un 'from_to_record' de nueva creación.
  • Este 'from_to_record' se escribe en la base de datos.
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>,
        ...
    },
}

Nota(s):

  • En cada codificación, se crea un nuevo 'from_to_record'.
  • Los 'from_to_records' nunca cambian, se crean una vez y siguen siendo válidos hasta que caducan.
  • No almacenamos 'from_id_to_ids' sino 'to_id_from_ids'. El motivo es que utilizamos "from_to_records" sólo para buscar un 'from_id' a partir de un 'to_id'.
  • Aquí mostramos un único 'from_to_record' con un tipo de columna JSONB para almacenar múltiples pares. Por supuesto, también puede tener varios registros.
  • Un 'user_account_id' conecta un registro a un user específico.

Descodificación

Cuando recibimos datos del cliente

  • Primero obtenemos 'from_to_records' no caducados de la base de datos utilizando los campos 'user_account_id' y 'created_on'.
  • A continuación, utilizando estos registros, sustituimos el 'to_ids' en los datos del cliente por el 'from_ids'.

Ventajas e inconvenientes

Como ya se ha mencionado, sea cual sea el método que desee utilizar, deberá realizar un trabajo adicional. He aquí algunos pros y contras de este método:

Pros:

  • Sustitución directa.
  • Casi sin cambios en el cliente.
  • La generación del nuevo UUID4s es una función optimizada.
  • No se tocan los registros originales.
  • Restricciones temporales sencillas al utilizar una fecha/hora de creación.

Contras:

  • Requiere una base de datos.
  • No es muy rápido.

Ejemplo: Flask aplicación con un formulario

El siguiente ejemplo, muy limitado, demuestra cómo funciona esto. En el ejemplo, podemos listar y editar miembros sin exponer las claves primarias reales.

Utilizamos Flask-Caching (FileSystemCache) como base de datos para los miembros y para el 'from_to_records'. Lo normal sería utilizar un sistema de base de datos real para los miembros y algo como Redis para el 'from_to_records'.

Crear un virtual environment y luego:

pip install flask
pip install Flask-Caching

Hay tres clases:

  • 'IdFromTo'
    Se utiliza para recuperar y guardar 'from_to_records' y para traducir el 'from_ids' a 'to_ids', y viceversa.
  • 'Db'
    Consultas a la base de datos
  • 'DbWrapper'
    Nuestros nuevos métodos gestionan los 'from_ids' y 'to_ids' en las peticiones.

El árbol del proyecto:

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

Aquí están los dos archivos:

# 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

Para ejecutar, vaya al directorio del proyecto y escriba:

python run.py

A continuación, dirija su navegador a:

127.0.0.1:5050

Resumen

Hay muchas maneras de ocultar su base de datos IDs. La solución presentada es para una base de datos que utiliza claves primarias UUID4 .

Antes de enviar los datos al cliente, los valores primarios UUID4s se sustituyen por nuevos UUID4s, y los valores originales y de sustitución se almacenan. Cuando se reciben datos del cliente, se cargan los valores originales y de sustitución y se sustituyen. Si esperamos demasiado, los valores caducan y debemos empezar de nuevo.

Sólo se requieren cambios mínimos en el cliente.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.