Eén regel code kan je volledige toegangsbeheer-model tenietdoen. Spreid de request body direct uit in een database-update en je geeft de client een pen in handen om je schema te herschrijven. Dat is mass assignment. Het is geen exotische bug of een uitzonderlijk geval. Het is een ontwerpfout die optreedt zodra een API een payload behandelt als zijn eigen updatebeleid.
await db.users.update(req.params.id, { ...req.body });
Het ziet er schoon uit. Het bespaart typewerk. Maar de client heeft controle over de keys. Een aanvaller kan "role": "admin", "accountId": "someone_else" of "credit": 99999 toevoegen aan een verder gewone profielupdate. Je validatielaag controleert misschien of die waarden strings of getallen zijn en zegt dat ze er prima uitzien. Validiteit is echter geen autorisatie. Een gebruiker kan legitiem eigenaar zijn van het doelrecord. Dat betekent niet dat hij het recht heeft om elk veld daarin te bewerken.
Hoe mass assignment er in de praktijk uitziet
Het gevaar schuilt in het gemak. Frameworks en ORM's maken het triviaal om JSON-keys direct te mappen op databasekolommen. Wanneer je dit doet, vertel je de database om de client te vertrouwen over wat er moet veranderen, en niet alleen over hoe het moet veranderen.
Een gebruiker die zijn profiel bijwerkt, stuurt mogelijk geldige gegevens voor displayName en bio, maar glipt daar role of balance tussendoor. Als je controller het object simpelweg doorstuurt, schrijft de database alles weg. Validatie vangt ongeldige waarden op. Het vangt zelden kwaadaardige keys op. De bedrijfsregel die zegt "deze gebruiker mag zijn profiel bijwerken" wordt een algemene toestemming voor elke kolom in de rij.
De oplossing is niet meer validatie. Het is een striktere architectuur.
De drie poorten
Een veilige mutatie moet drie afzonderlijke controles passeren voordat deze de opslag raakt.
Geaccepteerde velden (Allowlist)
Begin met het bepalen van de exacte keys waar je naar zult kijken. Als een veld niet op de allowlist staat, wijs de request dan af of verwijder de key. Dit draait de standaardhouding om: nieuwe databasekolommen zijn niet beschrijfbaar totdat een ontwikkelaar ze expliciet blootstelt. Schema's groeien in de loop van de tijd. Een teamgenoot voegt een stripeCustomerId, een departmentBudget of een isVerified flag toe. Met een allowlist zijn die nieuwe kolommen automatisch beschermd tegen schrijfacties van de client. Zonder allowlist is elke nieuwe kolom per ongeluk een blootgesteld API-oppervlak.
Geldige waarden
Zodra je weet welke velden zijn toegestaan, controleer je of de waarden logisch zijn. Is de tijdzone-string daadwerkelijk een erkende tijdzone? Is het e-mailadres correct geformatteerd? Valt het getal binnen een redelijke range? Dit is hygiëne. Het voorkomt dat rommel je systeem binnendringt, maar het stopt geen misbruik. Een volkomen geldige "admin" string is nog steeds gevaarlijk in het role veld als de verkeerde persoon hem verzendt.
Geautoriseerde overgangen
Dit is de poort die de meeste teams overslaan, en dit is waar de echte bescherming zit. Stel een gedetailleerde vraag: heeft deze specifieke actor toestemming om dit specifieke veld op dit specifieke record te muteren? Niet "is de gebruiker een admin?". Niet "heeft de gebruiker de write:users scope?". Maar eerder: "mag deze gebruiker zijn eigen displayName wijzigen, maar nooit zijn accountId?". Autorisatie per veld voorkomt dat een brede permissie zoals "Editor" of "User" een masterkey wordt voor elke eigenschap in de rij.
Het bouwen van de patch-functie
Koppel de drie poorten aan elkaar in één enkele pipeline. Wanneer een patch-request binnenkomt, loop je deze in volgorde door de verschillende stadia.
Ten eerste: filter de input tegen je allowlist. Als role geen toegestaan veld is voor dit endpoint, stop dan direct. Er is geen reden om een waarde te valideren of te autoriseren die je nooit had mogen ontvangen.
Ten tweede: valideer de toegestane waarden. Controleer types, formaten en bedrijfsregels. Een locatieveld moet een string zijn die naar een echte tijdzone verwijst. Een avatar-URL moet een geldige URI zijn met een bepaalde maximale lengte.
Ten derde: autoriseer de actie. Controleer of de actor eigenaar is van het doelrecord, of de exacte permissie bezit die vereist is voor dit veld. Eigenaarschap is een goede standaard voor persoonlijke gegevens, maar sommige velden hebben nog extra poorten nodig. Een gebruiker is misschien eigenaar van zijn eigen profiel, maar alleen een facturatiebeheerder zou taxRegion mogen aanpassen.
Ten vierde: normaliseer de data. Verwijder witruimte, voeg herhaalde spaties samen, zet e-mailadressen om naar kleine letters of verwijder besturingskarakters. Doe dit na de validatie maar vóór de opslag, zodat je niet met "vuile" strings vergelijkt tijdens de autorisatiecontroles.
Als de input een controle niet doorstaat, wijs dan de gehele mutatie af. Pas veilige velden niet gedeeltelijk toe en laat de foutieve velden niet stilletjes vallen. Een gemengde reactie traint clients om elke mogelijke key te proberen en te kijken wat werkt. Faal expliciet.
De randgevallen die er echt toe doen
Verdedigingsmechanismen tegen mass assignment leven of sterven bij details die unit tests vaak missen.
Dubbele JSON-keys. Aanvallers kunnen payloads sturen zoals {"role": "user", "role": "admin"}. Afhankelijk van je HTTP-parser en framework kan de tweede waarde de eerste overschrijven voordat je applicatiecode het object ziet. Test dit gedrag op parserniveau. Als je framework stilletjes de laatste key accepteert, kijkt je allowlist misschien naar "user", terwijl de database "admin" ontvangt.
Geneste objecten, nulls en arrays. Ga er niet vanuit dat de payload plat is. Een client kan een beperkt veld verpakken in een genest object zoals { "profile": { "role": "admin" } }. Je allowlist moet recursief zijn als je schema dat ook is. Bepaal eveneens hoe je met null omgaat. Betekent het "negeer dit veld" of "verwijder dit veld"? En als er een array wordt verwacht, wijst je validator dan onverwachte structuren af, of cast hij een enkel object naar een array en laat hij het door?
Unicode-normalisatie. Twee strings kunnen voor een mens identiek lijken, terwijl het verschillende reeksen bytes zijn. Een gebruiker kan een samengestelde é sturen of een ontbonden e plus een combinerend accent. Als je autorisatiecontrole één keer normaliseert, maar je opslaglaag op een andere manier normaliseert, kun je eindigen met inconsistente gegevens of, erger nog, een bypass waarbij een gebruikersnaam-botsing je logica omzeilt. Normaliseer vroegtijdig en normaliseer consistent.
Race conditions. Autorisatiebeslissingen zijn geen stilstaande beelden. Ze vinden plaats op een specifiek moment. Twee verzoeken kunnen hetzelfde record lezen, beiden zien dat de actor mag schrijven, en beiden updates uitvoeren. Tussendoor kan de status of de rechten van de actor zijn gewijzigd. Pas database-updates altijd toe met een voorwaarde op een versienummer of een state machine-waarde. Gebruik iets als UPDATE users SET ... WHERE id = ? AND version = 5. Als de rij is gewijzigd sinds je deze hebt gelezen, mislukt de schrijfactie. Behandel de fout door opnieuw te proberen of door het verzoek af te wijzen. Dit voorkomt dat verouderde autorisatiecontroles je gegevens corrumperen.
Monitor wat belangrijk is
Je kunt niet beveiligen wat je niet kunt zien. Bouw je audit-logging rond de beslissing, niet alleen rond de actie.
Log de actor-ID en de target-ID. Log de exacte veldnamen die zijn geaccepteerd en de velden die zijn geweigerd. Log de versie van het beleid (policy) die de beslissing heeft genomen en het uiteindelijke resultaat. Als een gebruiker plotseling ziet dat role wordt geweigerd bij het bijwerken van hun profiel, wil je dat direct weten.
Log nooit bearer tokens. Dump nooit volledige request bodies in je logs. Een audit trail moet je helpen misbruik te onderzoeken, en niet een opslagplaats worden van inloggegevens en persoonlijke gegevens.
De enige regel die je nodig hebt
Een request body stelt data voor. Het definieert nooit zijn eigen autoriteit. De client kan alles vragen. Jouw server beslist, veld voor veld en rij voor rij, wat er in de permanente opslag mag worden opgeslagen. Bouw je patches met die scheiding in gedachten, en mass assignment wordt een probleem dat je al hebt opgelost lang voordat het je autorisatielaag bereikt.
