Ausblenden der Primärschlüssel der Datenbank UUID Ihrer Webanwendung
Machen Sie es anderen nicht leicht, mit den Daten zu spielen, die Ihre Webanwendung preisgibt.
Wenn Sie eine Webanwendung erstellen, müssen Sie sehr darauf achten, nicht zu viele Informationen preiszugeben. Wenn Sie in Ihrer Datenbank Integer IDs (auto-increment) verwenden, dann geben Sie wahrscheinlich schon zu viel preis. Einige Beispiele. Eine Integer user_id macht es einfach zu erraten, wie viele neue Registrierungen Sie täglich erhalten. Ein Integer order_id macht es leicht zu erraten, wie viele Bestellungen Sie jeden Tag erhalten.
Darüber hinaus können Besucher versuchen, diese Werte in URLs oder Formularen zu erhöhen oder zu verringern. Wenn Sie nicht über die richtige Logik verfügen, können sie möglicherweise zuvor registrierte users und frühere Bestellungen sehen.
Viele dieser Probleme verschwinden, wenn UUID4s als Primärschlüssel verwendet wird. Dies macht es unmöglich, frühere Werte zu erraten. Sie müssen sich aber immer noch gegen Brute-Force-Raten schützen.
Aber auch UUID4s wollen wir manchmal nicht preisgeben. Es gibt viele Methoden, um Ihre IDs zu verstecken, hier stelle ich eine weitere vor. Die Annahme ist, dass Sie bereits UUID4s als Primärschlüssel verwenden. Beachten Sie, dass es hier keine wirklich einfachen Lösungen gibt.
Wie immer mache ich dies auf Ubuntu 22.04.
Wie es funktioniert
Die Annahme ist, dass Sie bereits UUID4s für Primärschlüssel (und Fremdschlüssel) verwenden.
Kodierung
Bevor wir die Daten vom Server zum Client senden, kodieren wir die IDs in den Daten:
- Wir ersetzen alle Werte des Primärschlüssels UUID4s , die 'from_ids', durch neue UUID4s -Werte, die 'to_ids'.
- Diese ursprüngliche und die neue UUIDs werden in einer neu angelegten 'from_to_record' gespeichert.
- Diese 'from_to_record' wird in die Datenbank geschrieben.
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>,
...
},
}
Hinweis(e):
- Bei jeder Kodierung wird eine neue 'from_to_record' angelegt.
- Die 'from_to_records' ändern sich nie, sie werden einmal angelegt und bleiben bis zum Ablauf gültig.
- Wir speichern nicht 'from_id_to_ids', sondern 'to_id_from_ids'. Der Grund dafür ist, dass wir 'from_to_records' nur verwenden, um aus einer 'to_id' eine 'from_id' nachzuschlagen.
- Hier zeigen wir eine einzelne 'from_to_record' mit einem Spaltentyp JSONB , um mehrere Paare zu speichern. Natürlich können Sie stattdessen auch mehrere Datensätze haben.
- Ein 'user_account_id' verbindet einen Datensatz mit einem bestimmten user.
Dekodierung
Wenn wir Daten vom Client erhalten:
- Zuerst holen wir nicht abgelaufene "from_to_records" aus der Datenbank mit Hilfe der Felder "user_account_id" und "created_on".
- Anhand dieser Datensätze ersetzen wir dann in den Daten des Mandanten die 'to_ids' durch die 'from_ids'.
Vor- und Nachteile
Wie bereits erwähnt, müssen Sie unabhängig von der Methode, die Sie verwenden möchten, zusätzliche Arbeit leisten. Hier sind einige Vor- und Nachteile dieser Methode:
Vorteile:
- Unkomplizierter Ersatz.
- Fast keine Änderungen auf dem Client.
- Die Generierung der neuen UUID4s ist eine optimierte Funktion.
- Die ursprünglichen Datensätze werden nicht berührt.
- Einfache zeitliche Einschränkung bei Verwendung eines Erstellungsdatums/Zeitlimits.
Nachteile:
- Erfordert eine Datenbank.
- Nicht sehr schnell.
Beispiel: Flask Anwendung mit einem Formular
Das folgende, sehr eingeschränkte Beispiel zeigt, wie dies funktioniert. In diesem Beispiel können wir Mitglieder auflisten und bearbeiten, ohne die eigentlichen Primärschlüssel offenzulegen.
Wir verwenden Flask-Caching (FileSystemCache) als Datenbank für die Mitglieder und für die 'from_to_records'. Normalerweise würden Sie ein echtes Datenbanksystem für die Mitglieder und etwas wie Redis für die "from_to_records" verwenden.
Erstellen Sie eine virtual environment und dann:
pip install flask
pip install Flask-Caching
Es gibt drei Klassen:
- 'IdFromTo'
Wird verwendet, um 'from_to_records' abzurufen und zu speichern und um die 'from_ids' in 'to_ids' zu übersetzen und umgekehrt. - 'Db'
Datenbankabfragen - DbWrapper
Unsere neuen Methoden zur Behandlung der 'from_ids' und 'to_ids' in Anfragen.
Der Projektbaum:
.
├── project
│ ├── app
│ │ └── factory.py
│ └── run.py
Hier sind die beiden Dateien:
# 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
Um sie auszuführen, gehen Sie in das Projektverzeichnis und geben Sie ein:
python run.py
Zeigen Sie dann mit Ihrem Browser auf:
127.0.0.1:5050
Zusammenfassung
Es gibt viele Möglichkeiten, Ihre Datenbank IDs zu verstecken. Die vorgestellte Lösung ist für eine Datenbank, die UUID4 Primärschlüssel verwendet.
Vor dem Senden der Daten an den Client werden die primären UUID4s -Werte durch neue UUID4s-Werte ersetzt, und die ursprünglichen und die Ersatzwerte werden gespeichert. Wenn Daten vom Client empfangen werden, laden wir die Original- und Ersatzwerte und ersetzen sie. Wenn wir zu lange warten, verfallen die Werte und wir müssen wieder von vorne anfangen.
Auf dem Client sind nur minimale Änderungen erforderlich.
Links / Impressum
Hiding, obfuscating or encrypting database IDs
https://bhch.github.io/posts/2021/07/hiding-obfuscating-or-encrypting-database-ids
Neueste
- Ausblenden der Primärschlüssel der Datenbank UUID Ihrer Webanwendung
- Don't Repeat Yourself (DRY) mit Jinja2
- SQLAlchemy, PostgreSQL, maximale Anzahl von Zeilen pro user
- Anzeige der Werte in den dynamischen Filtern SQLAlchemy
- Sichere Datenübertragung mit Public Key Verschlüsselung und pyNaCl
- rqlite: eine hochverfügbare und distverteilte SQLite -Alternative
Meistgesehen
- Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden
- Verwendung von UUIDs anstelle von Integer Autoincrement Primary Keys mit SQLAlchemy und MariaDb
- Verbindung zu einem Dienst auf einem Docker -Host von einem Docker -Container aus
- PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen
- SQLAlchemy: Verwendung von Cascade Deletes zum Löschen verwandter Objekte
- Flask RESTful API Validierung von Anfrageparametern mit Marshmallow-Schemas