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

Database UUID primaire sleutels van je webapplicatie verbergen

Maak het anderen niet gemakkelijk om te spelen met de gegevens die je webapplicatie blootstelt.

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

Als je een webapplicatie maakt, moet je heel voorzichtig zijn dat je niet te veel informatie vrijgeeft. Als je (auto-increment) Integer IDs gebruikt in je database, dan geef je waarschijnlijk al te veel prijs. Enkele voorbeelden. Een Integer user_id maakt het makkelijk om te raden hoeveel nieuwe inschrijvingen je elke dag ontvangt. Met een Integer order_id kun je gemakkelijk raden hoeveel bestellingen je elke dag ontvangt.

Bovendien kunnen bezoekers proberen deze waarden te verlagen of te verhogen in URLs of formulieren. Als je niet over de juiste logica beschikt, kunnen ze eerder geregistreerde user's en eerdere bestellingen zien.

Veel van deze problemen verdwijnen als UUID4s als primaire sleutels worden gebruikt. Dit maakt het onmogelijk om eerdere waarden te raden. Maar je moet je nog steeds wapenen tegen brute force raden.

Maar zelfs UUID4s willen we soms niet blootgeven. Er zijn veel methoden om je IDs te verbergen, hier presenteer ik er nog een. De aanname is dat je UUID4s al gebruikt als primaire sleutels. Merk op dat er hier niet echt gemakkelijke oplossingen zijn.

Zoals altijd doe ik dit op Ubuntu 22.04.

Hoe het werkt

De aanname is dat je UUID4s al gebruikt voor primaire sleutels (en foreign keys).

Codering

Voordat we de gegevens van de server naar de client sturen, coderen we de IDs in de gegevens:

  • We vervangen alle primaire sleutel UUID4s waarden, de 'from_ids', door nieuwe UUID4s waarden, de 'to_ids'.
  • Deze oorspronkelijke en nieuwe UUIDs worden opgeslagen in een nieuw gecreëerde "from_to_record".
  • Deze 'from_to_record' wordt naar de database geschreven.
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>,
        ...
    },
}

Opmerking(en):

  • Bij elke codering wordt een nieuwe 'from_to_record' aangemaakt.
  • from_to_records' veranderen nooit, ze worden eenmalig aangemaakt en blijven geldig tot ze verlopen.
  • We slaan 'from_id_to_ids' niet op, maar 'to_id_from_ids'. De reden hiervoor is dat we 'from_to_records' alleen gebruiken om een 'from_id' op te zoeken uit een 'to_id'.
  • Hier tonen we een enkele 'from_to_record' met een JSONB kolomtype om meerdere paren op te slaan. In plaats daarvan kun je natuurlijk ook meerdere records hebben.
  • Een 'user_account_id' verbindt een record met een specifieke user.

Decoderen

Wanneer we gegevens van de client ontvangen:

  • Eerst halen we niet-verlopen 'from_to_records' uit de database met behulp van de velden 'user_account_id' en 'created_on'.
  • Vervolgens vervangen we met deze records de 'to_ids' in de gegevens van de klant door de 'from_ids'.

Voor- en nadelen

Zoals gezegd, welke methode je ook wilt gebruiken, je moet extra werk doen. Hier zijn enkele voor- en nadelen van deze methode:

Voordelen:

  • Rechttoe rechtaan vervanging.
  • Bijna geen wijzigingen bij de client.
  • Het genereren van nieuwe UUID4s is een geoptimaliseerde functie.
  • We raken de originele records niet aan.
  • Eenvoudige tijdsbeperkingen bij gebruik van een aanmaakdatum/time-out.

Nadelen:

  • Vereist een database.
  • Niet erg snel.

Voorbeeld: Flask toepassing met een formulier

Het, zeer beperkte, voorbeeld hieronder laat zien hoe dit werkt. In het voorbeeld kunnen we leden opsommen en bewerken zonder de werkelijke primaire sleutels bloot te leggen.

We gebruiken Flask-Caching (FileSystemCache) als database voor de leden en voor de 'from_to_records'. Normaal gesproken zou je een echt databasesysteem gebruiken voor de leden en iets als Redis voor de 'from_to_records'.

Maak een virtual environment en dan:

pip install flask
pip install Flask-Caching

Er zijn drie klassen:

  • 'IdFromTo'
    Wordt gebruikt om 'from_to_records' op te halen en op te slaan en om de 'from_ids' te vertalen naar 'to_ids' en omgekeerd.
  • 'Db'
    Databasevragen
  • DbWrapper'
    Onze nieuwe methoden voor het afhandelen van 'from_ids' en 'to_ids' in aanvragen.

De projectboom:

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

Hier zijn de twee bestanden:

# 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

Ga om uit te voeren naar de projectmap en typ:

python run.py

Wijs vervolgens je browser op:

127.0.0.1:5050

Samenvatting

Er zijn veel manieren om je database IDs te verbergen. De gepresenteerde oplossing is voor een database die UUID4 primaire sleutels gebruikt.

Voordat de gegevens naar de klant worden verzonden, worden de primaire UUID4s waarden vervangen door nieuwe UUID4s en worden de oorspronkelijke en vervangende waarden opgeslagen. Wanneer we gegevens van de client ontvangen, laden we de originele en vervangende waarden en vervangen we ze. Als we te lang wachten, verlopen de waarden en moeten we opnieuw beginnen.

Er zijn slechts minimale wijzigingen nodig in de client.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.