IDOR vermijden via database
IDOR te vermijden in mijn applicatie.
Hoewel ik niet direct voorstander ben van security through obfuscation zie ik wel waarom dit handig zou kunnen zijn. Volgens OWASP is een makkelijke manier om alle ID's in de database te vervangen voor UUID's.
Hoewel dat mijn vrees voor collisions niet wegneemt, zag ik ook dat er ook een andere techniek van object reference mapping (niet: object relational mapping) bestaat, waarbij er UUID's gegenereerd worden en uniek gemaakt per sessie.
Dit alles heb ik nog nooit gedaan, dus ik denk dat ik het wel kan. Maar ik zie niet direct hoe dat laatste kunnen met een bestaande SQL database. Toch schijnen er frameworks te zijn die dat hebben ingebouwd.
Weet iemand hoe je dit kan implementeren op een bestaande PostgreSQL database?
Zijn er misschien tools waarmee het automatisch kan?
Ik kreeg onlangs de tip om Hoewel ik niet direct voorstander ben van security through obfuscation zie ik wel waarom dit handig zou kunnen zijn. Volgens OWASP is een makkelijke manier om alle ID's in de database te vervangen voor UUID's.
Hoewel dat mijn vrees voor collisions niet wegneemt, zag ik ook dat er ook een andere techniek van object reference mapping (niet: object relational mapping) bestaat, waarbij er UUID's gegenereerd worden en uniek gemaakt per sessie.
Dit alles heb ik nog nooit gedaan, dus ik denk dat ik het wel kan. Maar ik zie niet direct hoe dat laatste kunnen met een bestaande SQL database. Toch schijnen er frameworks te zijn die dat hebben ingebouwd.
Weet iemand hoe je dit kan implementeren op een bestaande PostgreSQL database?
Zijn er misschien tools waarmee het automatisch kan?
Verschillende UUID-versies ondersteunen het genereren van UUID’s op basis van een namespace. Dat kan onder andere met een MD5- of SHA1-hash, een URL of een object identifier (OID): je hebt daarmee meerdere mogelijkheden om een directe óf indirecte relatie te leggen tussen interne objecten en openbare UUID’s.
Voor PHP is ramsey/uuid overigens dé standaard library voor UUID's:
• https://github.com/ramsey/uuid
• https://uuid.ramsey.dev/en/stable/
Ik werk met PostgreSQL, en heb in de module uuid-ossp de functie uuid_generate_v5() gevonden.
Op het moment van schrijven is het nog vroeg, en ik zit nog aan de koffie, wat kan verklaren dat ik nog niet meteen de oplossing zie voor mijn applicatie om per sessie aparte ID's te maken. Ik denk even hardop terwijl ik typ:
- ik heb een lijst van actieve sessie ID's,
- daarnaast heb ik een paar schema's, met daarin verschillende tabellen met daarin allemaal rijen. De meeste rijen hebben een PK bestaande uit een bigint, sommige rijen hebben als PK een FK naar een andere tabel. Sommige tabellen zijn koppeltabellen met PK's bestaande uit ID's van meerdere tabellen. Daarbij heb ik natuurlijk ook gewone tabellen met PK's die uit meerdere ID's bestaan.
Wat ik nu doe is die ID's naar de browser sturen. Lekker makkelijk, maar dat wordt gezien als een 'direct object reference' of DOR. Je ziet niet het kenteken of EAN, alleen de integer van de database die wordt gegenereerd door een SEQUENCE van de tabel.
(Wanneer autorisatie goed is geregeld, is er feitelijk geen sprake van IDOR, want het is niet Insecure. Tenzij het hebben van DOR gelijk wordt geschakeld als Insecure. Maar daar gaat het nu niet over.)
De beste oplossing zou zijn om UUID's versie 5 te genereren per sessie. Mijn eerste gedachte is dat ik naast een sessie-tabel ook een "sessie_uuid" tabel moet hebben waarin per sessie gegenereerde UUID's heb.
Maar welke info gebruik je als namespace, en welke als name om de mapping naar de verschillende schema's, tabellen en rijen voor elkaar te krijgen?
Om de UUID's uniek te krijgen zou de sessie ID als namespace moeten dienen.
Dan zit ik nog met de mapping. Gezien dat het aantal schema's lager is dan 64, het aantal tabellen per schema lager dan 64, en het aantal rijen niet groter wordt dan 64-bit, zou dat getal als name kunnen dienen in de 128 bit van een UUID.
Alleen is het dan nog steeds Insecure als het lagere deel van een UID constant is.
Dan is de enige oplossing die ik zie om de name random te maken, en in de "sessie_uuid" tabel ook de namen van de schema's, kolommen, en ID's op te nemen als JSON... ofzo.
Ik heb het idee dat ik het wiel opnieuw zit uit te vinden, dit moet toch beter kunnen?
Toevoeging op 14/06/2024 07:17:51:
[Een dag later..]
Na een poosje mijmeren ben ik tot de conclusie gekomen dat hoewel sessies worden opgeslagen in de database, de database zelf niet de plek is om het probleem op de lossen. Want de applicatieserver communiceert met de browser, dat is de laag die beveiligd moet worden omdat de applicatieserver de sessies regelt.
Ik heb nu twee plaatsen bedacht waar IDOR op zou kunnen treden.
In URLs en in namen van HTML-formuliervelden.
URL's worden aangemaakt via een functie, die dan net zo goed de URL parameters per sessie in een tabel kan opslaan, met een UUID als ID. Wanneer de URL met UUID wordt gebruikt kan de front controller de parameters weer ophalen en zit er niets waardevols in de browser.
En hetzelfde geldt voor namen van velden in formulieren. Gewoon met een key-value store.
Ik heb me nog afgevraagd of daarvoor een in-memory engine nodig zou zijn in PostgreSQL.
Maar na het lezen van deze SO thread en het feit dat de servers elk 32 GB RAM hebben, denk ik niet dat het verschil zal maken.
Dank voor het meedenken, ik ben toe aan implementatie. Maar eerst het weekeinde!
ID's in de database vervangen voor UUID's is een slecht idee.
Vanwege datalocalisatie werkt indexering minder goed.
Bron: https://www.cybertec-postgresql.com/en/unexpected-downsides-of-uuid-keys-in-postgresql/
surrogaatsleutels — en dat liefst alleen voor de aggregate roots als je domain-driven design (DDD) goed toepast.
Technisch valt er ook wat op af te dingen. Met de functie UUID_TO_BIN() in MySQL kun je UUID's binair opslaan, wat ruimte bespaart. Deze functie heeft bovendien een flag die de bytevolgorde optimaliseert voor indexen.
Als je, bijvoorbeeld, het eerste hexadecimale cijfer in UUID's gebruikt voor een server-ID, kun je entiteiten zelfs sneller lezen in een cluster met tot en met 16 databaseservers. En dat is héél groot.
Je moet UUID's ook niet gebruiken als primaire sleutels maar als Technisch valt er ook wat op af te dingen. Met de functie UUID_TO_BIN() in MySQL kun je UUID's binair opslaan, wat ruimte bespaart. Deze functie heeft bovendien een flag die de bytevolgorde optimaliseert voor indexen.
Als je, bijvoorbeeld, het eerste hexadecimale cijfer in UUID's gebruikt voor een server-ID, kun je entiteiten zelfs sneller lezen in een cluster met tot en met 16 databaseservers. En dat is héél groot.
Ik denk niet dat ik DDD nu aan het toepassen ben, omdat mijn insteek 180 graden anders is; bij mij is de database zelf leidend ("Database First Approach"). Ik modelleer eerst de tabellen, en dan juist niet volgens DDD omdat mijn ervaring is dat business domains elkaar nogal eens overlappen. Ik modelleer eerder volgens SRP, waarbij tabellen zelfstandig naamwoorden zijn en kolommen bijvoeglijke naamwoorden.
In mijn code gebruik ik ActiveRecord waar het kan, ook sequentieel in transacties, in ieder geval voor alle INSERT, UPDATE en DELETE acties. Maar ik heb daarnaast ook duizenden regels aan custom SQL voor de ingewikkelde logica. De werking van het programma hangt af van controletabellen en autorisatie in de database.
Als ik naar het linkje van surrogaatsleutels ga, lees ik dat PK's ook surrogaatsleutels zijn. Uit aggregate roots maak ik op dat je alleen de sleutels wilt van de 'hoofdingang' van een model, dat klinkt allemaal heel erg logisch en database-achtig. Maar DDD is een software-design paradigma, niet voor databases en ook niet voor AR. Met de voorbeelden die gegeven worden komt het op mij over alsof je dan het idee van SQL en autorisaties opnieuw zelf moet nabouwen in objectgeoriënteerde code, op een UML-achtige manier. En dan ook nog eens in een OOP-taal.
Ik zocht een oplossing voor PostgreSQL, omdat de applicatieservers geschreven zijn in Rust. PostgreSQL slaat UUID's standaard binair op in 16 bytes / 128 bit int, met het eigen datatype UUID. Ik wil er van uitgaan dat de opslag in PostgreSQL ook is geoptimaliseerd, en dat MySQL er waarschijnlijk ook moeite heeft met het indexeren van random UUID's. Want het lijkt me dat het probleem van localiteit van data (uit in het artikel dat ik heb gelinkt in mijn vorige post) databases overstijgt.
Als laatste wil ik nog opmerken dat in PostgreSQL een andere verdeling bestaat en naamsverwarring over database clusters. Je kunt op een machine de database server of software installeren. Elke instantie of ("service") draait heeft een eigen cluster van geen of meer databases. Elke database heeft een of meer schema's, elk schema een of meer tabellen etc.
PostgreSQL staat bekend om zijn schaalbaarheid.
Persoonlijk heb ik schaalbaarheid nog niet nodig gehad, door het efficiënt modelleren van het ERD en de vooral tekstuele data worden databases bij mijn programma niet veel groter dan een paar GB.