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.
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
Recientes
- Gráfico de series temporales con Flask, Bootstrap y Chart.js
- Utilización de IPv6 con Microk8s
- Uso de Ingress para acceder a RabbitMQ en un clúster Microk8s
- Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
- Programación básica de trabajos con APScheduler
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
Más vistos
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados