Direct naar inhoud

NoSQL injection

CWE-943OWASP A03:2021Bijgewerkt 31 augustus 20266 min leestijd

NoSQL injection is een kwetsbaarheid waarbij een aanvaller de betekenis van een databasequery verandert door in plaats van een waarde een query-operator mee te sturen, bijvoorbeeld $ne of $gt in een JSON-body naar MongoDB. Daarmee omzeilt hij een inlogcontrole of leest hij documenten die niet voor hem bestemd zijn. De oplossing is schemavalidatie en het expliciet casten van invoer naar het verwachte type.

NoSQL-databases zoals MongoDB werken niet met tabellen en SQL, maar met documenten. Het injectieprobleem verdwijnt daarmee niet, het verandert alleen van vorm: in plaats van een stukje SQL smokkelt een aanvaller een query-operator naar binnen. Hieronder leest u hoe dat werkt, wat een aanvaller ermee bereikt en hoe u het voorkomt.

Wat is NoSQL injection?

NoSQL injection is een kwetsbaarheid waarbij een aanvaller de betekenis van een databasequery verandert door invoer mee te sturen die de applicatie als losse waarde verwacht, maar die in werkelijkheid een stuk querytaal is. NoSQL staat voor “Not only SQL”, een verzamelnaam voor databases die geen tabellen en geen SQL gebruiken. MongoDB is daarvan veruit de bekendste.

Het verschil met SQL injection zit in de vorm. Een SQL-query is een tekstregel, en een aanvaller plakt daar tekst in. Een MongoDB-query is geen tekst maar een gestructureerd object: een verzameling velden met waarden, waarbij een waarde ook een operator mag zijn, zoals $ne (niet gelijk aan) of $gt (groter dan). Zodra invoer van de gebruiker rechtstreeks als waarde in dat object terechtkomt en die invoer zelf een object blijkt te zijn, leest de database hem als operator in plaats van als gegeven.

Een alledaagse vergelijking: aan een balie vult u een formulier in met een achternaam en een geboortedatum. Iemand schrijft in het vakje geboortedatum echter geen datum, maar de zin “maakt niet uit, elke datum is goed”. Een medewerker die het formulier letterlijk in het zoeksysteem overneemt, krijgt vervolgens iedereen te zien.

Dat maakt JSON-API’s het natuurlijke aangrijpingspunt: een HTML-formulier levert altijd tekst op, terwijl JSON het verschil tussen een tekst en een object ongeschonden doorgeeft aan uw code.

Hoe werkt een NoSQL injection-aanval?

Neem een inlogendpoint in Node.js dat het verzoek als JSON ontvangt en de twee velden zonder tussenkomst in een MongoDB-query plaatst.

Kwetsbaar:

// De velden uit req.body komen rechtstreeks uit de JSON van de aanvrager
app.post("/api/login", async (req, res) => {
  const user = await db.collection("users").findOne({
    email: req.body.email,
    password: req.body.password
  });

  if (user) {
    req.session.userId = user._id;
    return res.json({ ok: true });
  }
  res.status(401).json({ error: "invalid credentials" });
});

Bij een normale aanmelding stuurt de browser twee teksten en werkt dit precies zoals bedoeld. Een aanvaller stuurt echter geen teksten, maar twee kleine objecten:

POST /api/login HTTP/1.1
Host: app.example.com
Content-Type: application/json

{"email": {"$ne": null}, "password": {"$ne": null}}

De query die MongoDB nu uitvoert, zoekt naar een document waarvan het e-mailadres niet gelijk is aan null en het wachtwoord evenmin. Dat geldt voor vrijwel elke gebruiker in de collectie. findOne levert de eerste treffer terug, de applicatie ziet een gevonden gebruiker en zet een sessie op. De aanvaller is binnen zonder één geldig gegeven. Vult hij het e-mailadres van een bekende beheerder wél gewoon in en laat hij alleen het wachtwoord een operator zijn, dan kiest hij bovendien zelf welk account hij overneemt.

Het blijft niet bij aanmelden. Met de operator $regex kan een aanvaller een wachtwoord, resetcode of API-sleutel teken voor teken afleiden: hij vraagt telkens of de opgeslagen waarde met een bepaald patroon begint en leest uit het verschil in antwoord af of dat klopt. Dat is dezelfde werkwijze als blind SQL injection, alleen met andere bouwstenen.

De oplossing bestaat uit twee stappen die elkaar aanvullen. Controleer het verzoek tegen een expliciet schema voordat het de database bereikt, en dwing af dat elke waarde het type heeft dat u verwacht.

Veilig:

import { z } from "zod";

const LoginBody = z.object({
  email: z.string().email().max(254),
  password: z.string().min(8).max(200)
});

app.post("/api/login", async (req, res) => {
  const parsed = LoginBody.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: "invalid input" });
  }

  // Alleen een string bereikt de query; een object komt hier niet meer langs
  const email = String(parsed.data.email).toLowerCase();
  const user = await db.collection("users").findOne({ email });

  if (!user || !(await bcrypt.compare(parsed.data.password, user.passwordHash))) {
    return res.status(401).json({ error: "invalid credentials" });
  }

  req.session.userId = user._id;
  res.json({ ok: true });
});

Er zijn twee dingen veranderd. Het schema weigert elk verzoek waarin het e-mailadres of het wachtwoord geen tekst is, dus $ne komt niet eens langs de voordeur. En het wachtwoord wordt niet meer in de query vergeleken maar achteraf tegen een hash, waardoor de database niet langer over de aanmelding beslist. De expliciete cast met String() is de laatste vangrail voor waarden die u van elders krijgt.

Het wegfilteren van sleutels die met een dollarteken beginnen is een pleister, geen oplossing: een blokkeerlijst dekt alleen de patronen die u zelf bedacht heeft en beschadigt legitieme invoer. Let er bovendien op dat dit niet tot JSON beperkt blijft. Met de uitgebreide query-parser van Express verandert een URL als ?email[$ne]=x net zo goed in een genest object.

Wat is de impact van NoSQL injection?

De ernst hangt af van de plek waar de invoer in de query belandt, en het bereik is groot. Zit de kwetsbaarheid in een aanmelding, dan is het gevolg direct en volledig: een aanvaller logt in zonder wachtwoord, desgewenst als beheerder, en heeft daarna alle rechten van dat account. Zit zij in een zoek- of filterfunctie, dan is het gevolg subtieler: een filter dat alleen de eigen documenten hoorde te tonen, laat zich verbreden tot de hele collectie, inclusief die van andere klanten in een gedeelde omgeving.

Daarnaast is gerichte onttrekking mogelijk. Met een $regex-aanval reconstrueert een aanvaller precies de velden die het meest waard zijn: wachtwoord-hashes, sessietokens, resetcodes. Bereikt de invoer een update-operatie, dan kan hij ook documenten wijzigen, bijvoorbeeld zijn eigen rol ophogen.

Aan de bovenkant van het spectrum staat uitvoering van code. Constructies als $where en mapReduce voeren JavaScript uit op de databaseserver, zodat een injectie daar kan uitgroeien tot het volledig uitlezen van gegevens of tot een denial of service door een oneindige lus. Daarom loopt de ernst van hoog tot kritiek, met bovendien een meldplicht bij de Autoriteit Persoonsgegevens zodra persoonsgegevens zijn geraakt.

Hoe spoort u NoSQL injection op?

De kern van het testwerk is typemanipulatie: stuur op elke plek waar de applicatie een tekst verwacht een object mee, en vergelijk het antwoord. Een verzoek waarin een veld een operator als $ne of $gt bevat en dat opeens een succesvolle aanmelding, meer resultaten of een langere responstijd oplevert, is een sterk signaal. Een serverfout met een Mongo-stacktrace is dat evengoed, want die verraadt dat de invoer de query ongehinderd bereikt heeft.

Automatische scanners missen dit vaak, om een begrijpelijke reden: zij muteren de tekst in een parameter, niet het type ervan. Een scanner die ' OR 1=1 achter elke waarde plakt, komt bij een JSON-API nooit verder dan een afgekeurde string. Een code review helpt snel: zoek naar aanroepen van find en findOne waarin een waarde uit req.body of req.query rechtstreeks in het filter staat. Bij een penetratietest onderzoekt AssistSec deze type-afhankelijke gevallen handmatig, omdat juist zij door geautomatiseerde controles heen glippen.

Hoe voorkomt u NoSQL injection?

  • Valideer elk verzoek tegen een expliciet schema met een bibliotheek als zod, Joi of ajv, en wijs onbekende velden en verkeerde types af voordat de applicatie iets met de data doet.
  • Cast waarden expliciet naar het verwachte type. Een String() of Number() vlak voor de query maakt het onmogelijk dat er nog een object in het filter staat.
  • Gebruik een ODM met een strikt schema, zoals Mongoose, en vermijd velden van het type Mixed en zelf samengestelde filterobjecten.
  • Vergelijk wachtwoorden en tokens nooit in de database. Haal het document op aan de hand van een identificerend veld en vergelijk de geheime waarde daarna in de applicatie tegen een hash.
  • Schakel serverside JavaScript uit op de databaseserver als u $where en mapReduce niet nodig heeft, en beperk de rechten van de databasegebruiker tot wat de applicatie echt gebruikt.
  • Toon nooit ruwe databasefouten aan de gebruiker. Log ze intern; een stacktrace vertelt een aanvaller precies waar zijn invoer terechtkomt.
  • Laat de API periodiek testen. Een gerichte penetratietest en een code review vangen de type-afhankelijke gevallen die scanners overslaan.

Bronnen

Veelgestelde vragen

Is NoSQL injection minder gevaarlijk dan SQL injection?

Nee. De vorm verschilt, de gevolgen niet. Een geslaagde NoSQL injection levert net zo goed een omzeilde inlogcontrole, een uitgelezen collectie of een aangepast document op. In een enkel geval reikt de impact zelfs verder, omdat sommige NoSQL-databases JavaScript op de server kunnen uitvoeren.

Beschermt Mongoose tegen NoSQL injection?

Grotendeels wel, want een Mongoose-schema cast waarden naar het gedeclareerde type en weigert wat niet past. De bescherming vervalt zodra u een filter zelf samenstelt uit onbewerkte invoer, een veld van het type Mixed gebruikt of de strikte modus uitzet.

Welke MongoDB-operatoren gebruiken aanvallers het vaakst?

In de praktijk zijn dat $ne en $gt om een vergelijking altijd waar te maken, $regex om een waarde teken voor teken af te leiden, $in om een reeks waarden tegelijk te proberen en $where om JavaScript op de server af te dwingen.

Helpt het om dollartekens uit invoer te verwijderen?

Beperkt. Het vangt de meest voorhandene payloads af, maar het is een blokkeerlijst en die is nooit volledig. Bovendien beschadigt het legitieme invoer. Typecontrole en schemavalidatie lossen het probleem bij de wortel op.

Loopt een GraphQL- of REST-API met JSON extra risico?

Ja, omdat JSON het verschil tussen een tekst en een object ongeschonden doorgeeft aan uw code. Een klassiek HTML-formulier levert altijd tekst op; een JSON-body kan een genest object bevatten dat de database als operator leest.

Verwante artikelen

Druk op / om te zoeken · Esc