Direct naar inhoud

Mass assignment

CWE-915OWASP A01:2021Bijgewerkt 31 augustus 20266 min leestijd

Mass assignment is een kwetsbaarheid waarbij een aanvaller velden overschrijft die niet voor hem bedoeld zijn, doordat de applicatie een volledige request body op een model bindt. Eén extra veld als isAdmin of balance in de JSON is genoeg om beheerder te worden. De oplossing is een expliciete allowlist van toegestane velden, bij voorkeur via een apart DTO.

Bijna elke applicatie neemt velden uit een formulier of een JSON-body over in een object dat vervolgens naar de database gaat. Frameworks maken dat aantrekkelijk kort: één regel code zet alle velden uit het verzoek in één keer op het model. Precies daar ontstaat mass assignment, want vanaf dat moment bepaalt de afzender van het verzoek welke velden meekomen, en niet u.

Wat is mass assignment?

Mass assignment is een kwetsbaarheid waarbij een aanvaller velden van een object kan overschrijven die niet voor hem bedoeld waren, doordat de applicatie een volledige request body ongefilterd op een model bindt. In andere ecosystemen heet hetzelfde probleem autobinding, object injection of over-posting; de OWASP API Security Top 10 rangschikt het onder Broken Object Property Level Authorization.

Een alledaagse vergelijking: u schrijft zich in bij een sportvereniging en vult een formulier in met naam, adres en geboortedatum. Onderaan dat formulier staan twee vakjes die normaal alleen de ledenadministratie invult, namelijk “contributie voldaan” en “bestuurslid”. Neemt de medewerker het ingevulde formulier ongelezen over in het ledensysteem, dan bepaalt de aanvrager zelf of hij betaald heeft en of hij in het bestuur zit. Mass assignment is precies die ongecontroleerde overname.

De kern van het probleem is dat een model bijna altijd meer velden heeft dan het formulier toont. Een gebruikersrecord bevat naast naam en e-mailadres ook velden als isAdmin, role, emailVerified, organisationId, credits of passwordHash. Het formulier laat er drie zien, de binding accepteert ze allemaal. Een aanvaller hoeft niets te omzeilen: hij voegt eenvoudig een veld toe aan de JSON die hij toch al verstuurt.

Daarmee valt mass assignment onder Broken Access Control. Niet de authenticatie faalt, maar de autorisatie op veldniveau. De gebruiker mag zijn eigen profiel wijzigen, dat klopt. Hij mag alleen niet elk veld van dat profiel wijzigen, en juist dat onderscheid ontbreekt.

Hoe werkt een mass assignment-aanval?

Neem een endpoint waarmee een ingelogde gebruiker zijn eigen profiel bijwerkt. De ontwikkelaar haalt het gebruikersobject op, kopieert de request body erop en slaat het weer op.

Kwetsbaar:

app.patch("/api/users/me", requireLogin, async (req, res) => {
  const user = await User.findById(req.session.userId);

  // De volledige request body wordt op het model gezet
  Object.assign(user, req.body);
  await user.save();

  res.json({ id: user.id, email: user.email });
});

De autorisatie ziet er op het eerste gezicht in orde uit: requireLogin weert anonieme bezoekers en de gebruiker bewerkt uitsluitend zijn eigen record. Toch bepaalt req.body volledig welke kolommen worden geschreven. De aanvaller opent zijn profielpagina, onderschept het verzoek en voegt twee velden toe:

PATCH /api/users/me HTTP/1.1
Host: app.example.com
Content-Type: application/json
Cookie: session=b1c9f4a2

{"displayName":"Mo","isAdmin":true,"credits":100000}

De namen van die velden hoeft hij niet te raden. Ze staan meestal gewoon in de respons van het GET-endpoint van hetzelfde object, in een OpenAPI-schema, in een GraphQL-introspectie of in de JavaScript-bundel die het beheerdersscherm meelevert. Na dit ene verzoek is de aanvaller beheerder. Er is geen injectie, geen exploit en geen afwijkend verkeerspatroon: het is een geldig verzoek naar een geldig endpoint, met twee velden extra.

De oplossing is de request body niet als geheel te vertrouwen, maar er expliciet uit te halen wat een gebruiker over zichzelf mag wijzigen. Dat doet u met een DTO, voluit een data transfer object: een apart type dat precies de toegestane velden beschrijft en de rest weigert.

Veilig:

import { z } from "zod";

// DTO: precies de velden die een gebruiker over zichzelf mag wijzigen
const ProfileUpdate = z
  .object({
    displayName: z.string().min(1).max(80),
    locale: z.enum(["nl", "en", "de"])
  })
  .partial()
  .strict(); // een onbekend veld levert een fout op, geen stilte

app.patch("/api/users/me", requireLogin, async (req, res) => {
  const parsed = ProfileUpdate.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: "Ongeldig veld in de request body" });
  }

  const user = await User.findById(req.session.userId);

  // parsed.data bevat uitsluitend velden uit de allowlist
  Object.assign(user, parsed.data);
  await user.save();

  res.json({ id: user.id, email: user.email });
});

Er zijn twee dingen veranderd. parsed.data bevat uitsluitend velden uit de allowlist, dus isAdmin bereikt het model niet meer, ongeacht wat er in de body stond. En door strict wordt een onbekend veld actief afgewezen in plaats van stilzwijgend genegeerd. Dat is het verschil tussen een aanvaller die rustig kan blijven proberen en een logregel die u laat zien dat iemand het probeert.

Dezelfde gedachte bestaat in elk framework onder een eigen naam: strong parameters in Rails, de eigenschap fillable in Laravel, een aparte DTO-klasse in Spring en een expliciete bindlijst in ASP.NET Core.

Een allowlist geldt per endpoint en per rol. Hergebruikt u hetzelfde DTO voor het beheerdersscherm en voor zelfbeheer, dan komt het gevaarlijke veld via de achterdeur alsnog binnen. Let ook op velden die onschuldig ogen: organisationId, teamId of roleId verplaatsen een account naar een andere klant of rol zonder dat er ooit isAdmin in de body staat.

Wat is de impact van mass assignment?

De impact hangt volledig af van welk veld schrijfbaar is, en daarom loopt de ernst van middel tot hoog. Is alleen een cosmetisch veld te overschrijven, bijvoorbeeld een tijdstempel of een intern notitieveld, dan blijft het een integriteitsprobleem zonder direct gevolg. Staat er een rechtenveld in het model, dan is het resultaat een volledige privilege escalation: de aanvaller maakt zichzelf beheerder en erft in één verzoek alles wat die rol mag.

Daartussen ligt de categorie die commercieel het meest pijn doet. Een schrijfbare balance, credits, price of discount maakt van een bestelproces een zelfbediening, waarbij de aanvaller zijn eigen saldo of prijs bepaalt. Een schrijfbare organisationId of tenantId doorbreekt de scheiding tussen klanten in een multitenant-omgeving, wat in de praktijk neerkomt op inzage in de gegevens van een andere klant. En een schrijfbare emailVerified, email of passwordResetToken levert een route naar accountovername op.

Zakelijk vertaalt dat zich naar gemanipuleerde omzet, een datalek met meldplicht en een integriteitsprobleem dat achteraf lastig te reconstrueren is. Dat laatste wordt onderschat: omdat het verzoek er volstrekt normaal uitziet, ontbreekt in veel logboeken het spoor dat laat zien wanneer en hoe een account beheerder werd.

Hoe spoort u mass assignment op?

Begin bij het leesmodel. Vraag een object op via de API en leg de velden in de respons naast de velden die het bijbehorende schrijfendpoint volgens de documentatie accepteert. Elk veld dat u wel terugkrijgt maar officieel niet mag versturen, is een kandidaat. Aanvullende bronnen zijn een OpenAPI-specificatie, een GraphQL-introspectie, verborgen invoervelden in formulieren, de JavaScript-bundel van het beheerdersscherm en de modeldefinities in de broncode.

Daarna test u door aan een verder normaal verzoek één extra veld toe te voegen en het object opnieuw op te vragen om te zien of de waarde echt is opgeslagen. Let daarbij op het verschil tussen genegeerd en verwerkt: een antwoord met status 200 zegt niets, de tweede uitlezing wel. Verandert de waarde niet zichtbaar in de respons maar wel in het gedrag, bijvoorbeeld doordat er ineens een beheerdersmenu verschijnt, dan is het effect er evengoed.

Geautomatiseerde scanners presteren hier zwak, omdat zij de betekenis van een veldnaam niet kennen en de gevolgen van een geslaagde binding niet kunnen beoordelen. Het opsporen is handwerk, met kennis van het datamodel en van de rollen in de applicatie. AssistSec neemt mass assignment mee in een penetratietest van API’s en webapplicaties, en toont per bevinding welk veld werd geschreven en wat een aanvaller daarmee bereikte.

Hoe voorkomt u mass assignment?

  • Bind nooit rechtstreeks op uw databasemodel. Laat een verzoek eerst landen in een DTO dat alleen de velden van dat ene endpoint kent, en kopieer daarna veld voor veld naar het model.
  • Werk met een allowlist, niet met een blokkeerlijst. Een lijst van verboden velden vergeet het veld dat volgende maand aan het model wordt toegevoegd; een lijst van toegestane velden niet.
  • Weiger onbekende velden expliciet. Antwoord met een 400 en leg de poging vast. Stil negeren is veiliger dan accepteren, maar levert u geen enkel signaal op.
  • Houd privilegevelden buiten elk schrijfpad van de gebruiker. Rollen, saldi, prijzen en verificatievlaggen worden serverzijdig gezet, via een aparte handeling met een eigen autorisatiecontrole.
  • Maak de allowlist rolafhankelijk. Wat een beheerder mag schrijven is een andere verzameling dan wat de eigenaar van een record mag schrijven; gebruik daarvoor twee schema’s in plaats van één schema met een uitzondering.
  • Zet de bescherming van uw framework aan en houd die actueel. Strong parameters in Rails, fillable in Laravel en expliciete bindlijsten in Spring en ASP.NET Core werken alleen als elk nieuw model eraan wordt toegevoegd.
  • Log wijzigingen aan gevoelige velden. Een auditregel bij elke wijziging van een rol, een saldo of een verificatievlag maakt misbruik achteraf zichtbaar en is vaak het enige spoor dat overblijft.

Bronnen

Veelgestelde vragen

Wat is het verschil tussen mass assignment en IDOR?

Bij IDOR benadert een aanvaller een heel object dat niet van hem is, meestal door een id in de URL te veranderen. Bij mass assignment blijft hij bij zijn eigen object, maar schrijft hij daarin een veld dat hij niet mag wijzigen. IDOR is een autorisatiefout op objectniveau, mass assignment een autorisatiefout op veldniveau. Beide vallen onder Broken Access Control en worden in een test naast elkaar onderzocht.

Beschermen moderne frameworks standaard tegen mass assignment?

Gedeeltelijk, en alleen als u de bescherming gebruikt. Rails heeft strong parameters, Laravel kent de eigenschap fillable, Spring en ASP.NET Core ondersteunen expliciete bindlijsten en aparte DTO-klassen. Die mechanismen zijn opt-in per model of per endpoint, dus een nieuw model of een snel toegevoegd endpoint valt er standaard buiten. De bescherming is zo actueel als de laatste ontwikkelaar die eraan dacht.

Is het genoeg om een gevoelig veld uit de API-documentatie te laten?

Nee. Een veld dat niet gedocumenteerd is, wordt evengoed gebonden zodra de naam bekend is, en die naam staat meestal gewoon in de respons van het leesendpoint, in een GraphQL-introspectie of in de JavaScript-bundel van het beheerdersscherm. Weglaten uit de documentatie verandert niets aan het gedrag van de server. Alleen een allowlist aan de serverzijde is een echte maatregel.

Hoe test ik of een endpoint kwetsbaar is voor mass assignment?

Vraag het object eerst op met een GET en noteer alle velden in de respons. Voeg daarna aan een normale schrijfactie één veld toe dat gevoelig lijkt, bijvoorbeeld isAdmin of role, en vraag het object opnieuw op. Een status 200 zegt niets; pas de tweede uitlezing laat zien of de waarde daadwerkelijk is opgeslagen. Let ook op indirecte effecten, zoals een beheerdersmenu dat na het verzoek zichtbaar wordt.

Verwante artikelen

Druk op / om te zoeken · Esc