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

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.

29 März 2024
post main image
https://unsplash.com/@davidkristianto

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

Mehr erfahren

Flask Security

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.