Wat is SQL injection, en hoe bescherm je je ertegen
Kort antwoord
SQL injection ontstaat wanneer onvertrouwde invoer rechtstreeks als tekst in een databasequery terechtkomt, waardoor een aanvaller kan veranderen wat die query daadwerkelijk doet. De standaardoplossing is een parameterized query (ook wel prepared statement genoemd). Die houdt gebruikersinvoer altijd als data, en laat het nooit onderdeel worden van de structuur van de query. Vrijwel elke moderne databasebibliotheek ondersteunt dit. Het is de echte oplossing, niet zomaar één laag tussen meerdere.
Hoe SQL injection in de praktijk ontstaat
Een SQL injection-kwetsbaarheid begint met onvertrouwde invoer, alles wat een gebruiker kan beïnvloeden zoals een formulierveld, een URL-parameter of een cookiewaarde, die rechtstreeks als platte tekst in een databasequery wordt geplakt. De applicatie bouwt de query door strings aan elkaar te plakken, stuurt het resultaat naar de database, en de database kan geen onderscheid maken tussen "de query die de ontwikkelaar bedoelde" en "de query zoals die er nu uitziet nadat de invoer is toegevoegd". Bevat die invoer zelf SQL-syntax, dan kan dat de betekenis van de query volledig veranderen. Een aanvaller die dit ontdekt, kan mogelijk data lezen die hij niet zou mogen zien, records aanpassen of ze verwijderen, allemaal via een veld dat alleen bedoeld was voor iets als een gebruikersnaam of een zoekterm.
Een eenvoudig voorbeeld
Stel je een inlogcontrole voor die zijn query in grote lijnen zo opbouwt: hij neemt de gebruikersnaam die in het formulier is getypt en plakt die rechtstreeks in een string die ongeveer zegt "zoek de gebruiker waarvan de gebruikersnaam gelijk is aan" wat er ook is ingetypt. Typt een legitieme gebruiker alice, dan vraagt de query om de gebruiker met de naam alice, precies zoals bedoeld. Maar omdat de gebruikersnaam als platte tekst is ingevoegd, kan een aanvaller iets typen dat helemaal geen gebruikersnaam is: een fragment SQL-syntax dat is ontworpen om de logica van de query te veranderen, bijvoorbeeld iets waardoor de "where"-voorwaarde altijd als waar wordt geëvalueerd, ongeacht welke gebruikersnaam werd verwacht. De database ziet geen "ongewone gebruikersnaam", maar een aangepaste query, en voert die uit zoals hij er staat.
Vergelijk dat nu met een inlogcontrole die in plaats daarvan een parameterized placeholder gebruikt voor de gebruikersnaam. De structuur van de query, "zoek de gebruiker waarvan de gebruikersnaam gelijk is aan een placeholder", staat vast en wordt eerst naar de database gestuurd. De getypte gebruikersnaam wordt daarna apart aangeleverd, als een waarde die in die placeholder wordt geplaatst, nooit als tekst die met de query zelf wordt samengevoegd. Wat de aanvaller ook typt, het wordt puur behandeld als de waarde waarnaar wordt gezocht, zelfs als het tekens bevat die in de samengevoegde versie gevaarlijk zouden zijn geweest. De logica van de query kan simpelweg niet worden veranderd door wat er in dat veld terechtkomt, omdat de database de structuur van de query en de data van de query vanaf het begin via twee gescheiden kanalen houdt.
De echte oplossing: parameterized queries
Parameterized queries, ook wel prepared statements genoemd, zijn de standaardoplossing, geen optionele extra. Ze werken door de structuur van de query los van de waarden die hem invullen naar de database te sturen, zodat gebruikersinvoer altijd als data wordt behandeld en nooit als onderdeel van de syntax van de query, ongeacht welke tekens erin zitten. Dit is geen nichefunctie: vrijwel elke moderne databasebibliotheek en ORM, in vrijwel elke programmeertaal, ondersteunt parameterized queries als een normale, voor de hand liggende manier om een query te schrijven. Bouwt een codebase ergens queries door strings met gebruikersinvoer aan elkaar te plakken, dan is dat precies het patroon dat het waard is om op te sporen en te vervangen.
Extra lagen, geen vervanging
Parameterized queries lossen het injectiemechanisme zelf op, maar twee andere praktijken zijn het waard om daarbovenop te leggen, als defence in depth in plaats van als alternatief:
- Input-validatie. Controleren of invoer eruitziet zoals verwacht, een e-mailveld lijkt op een e-mailadres, een numerieke ID is ook echt numeriek, vangt misvormde invoer vroeg af. Het is een nuttige extra controle, maar geen vervanging voor parameterized queries, want validatielogica kan onvolledig zijn of omzeild worden op manieren waarop de eigen parameterafhandeling van een databasedriver dat niet is.
- Databaseaccounts met minimale rechten. Het databaseaccount waarmee een applicatie verbinding maakt, zou alleen de rechten moeten hebben die die applicatie echt nodig heeft. Heeft een webapplicatie alleen ooit specifieke tabellen nodig om te lezen en te schrijven, dan zouden de bijbehorende databasegegevens ook geen rechten moeten hebben om tabellen te verwijderen of onverwante tabellen te lezen. Dit beperkt de schade als een injectiefout, of een andere inbreuk, toch gebeurt, zonder de fout zelf te voorkomen.
Beide zijn de moeite waard. Geen van beide vervangt het daadwerkelijk oplossen van het patroon in de querybouw dat injection om te beginnen mogelijk maakt.