jan-karel.nl
Home / Securitymaatregelen / Webbeveiliging / SQL Injection Preventie

SQL Injection Preventie

SQL Injection Preventie

SQL Zonder Slaaptekort

Webrisico is zelden mysterieus. Het zit meestal in voorspelbare fouten die onder tijdsdruk blijven staan.

Bij SQL Injection Preventie gaat het om strikte invoergrenzen, parameterized queries en reviews die query-risico vroeg stoppen.

Dat maakt security minder een losse controle achteraf en meer een standaardkwaliteit van je product.

Directe maatregelen (15 minuten)

Waarom dit telt

De kern van SQL Injection Preventie is risicoreductie in de praktijk. Technische context ondersteunt de maatregelkeuze, maar implementatie en borging staan centraal.

Verdediging: hoe het wel moet

We hebben nu een heel hoofdstuk besteed aan het breken van dingen. Laten we even praten over het repareren ervan. Want als je na het lezen van dit alles niet een vage misselijkheid voelt over je eigen code, heb je niet goed opgelet.

1. Parameterized queries — overal, altijd, zonder uitzonderingen

Dit is de enige verdediging die ertoe doet. Al het andere is een pleister op een geamputeerd been.

Python (psycopg2 / PyMySQL):

# GOED
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, password)
)

# SLECHT — doe dit nooit
cursor.execute(
    f"SELECT * FROM users WHERE username = '{username}'"
    f" AND password = '{password}'"
)

Java (JDBC):

// GOED
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();

// SLECHT
Statement s = conn.createStatement();
s.executeQuery(
    "SELECT * FROM users WHERE username = '" + username + "'"
);

PHP (PDO):

// GOED
$stmt = $pdo->prepare(
    "SELECT * FROM users WHERE username = :user
     AND password = :pass"
);
$stmt->execute([':user' => $username, ':pass' => $password]);

// SLECHT
$pdo->query(
    "SELECT * FROM users WHERE username = '$username'"
);

C# (.NET):

// GOED
using var cmd = new SqlCommand(
    "SELECT * FROM users WHERE username = @user", conn
);
cmd.Parameters.AddWithValue("@user", username);

// SLECHT
var cmd = new SqlCommand(
    $"SELECT * FROM users WHERE username = '{username}'", conn
);

Node.js (met mysql2):

// GOED
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password = ?',
  [username, password]
);

// SLECHT
const [rows] = await connection.query(
  `SELECT * FROM users WHERE username = '${username}'`
);

Het patroon is altijd hetzelfde: de query heeft placeholders (?, %s, :name, @name) en de waarden worden apart meegegeven. De database-driver zorgt ervoor dat de waarden als data worden behandeld, nooit als code.

2. ORM gebruiken

Object-Relational Mappers (ORMs) zoals SQLAlchemy, Hibernate, Entity Framework, en Sequelize genereren parameterized queries automatisch:

# SQLAlchemy — automatisch parameterized
user = session.query(User).filter_by(
    username=username, password=password
).first()

Maar pas op: ORMs beschermen je niet als je raw SQL gebruikt:

# Dit is WEER kwetsbaar, ook met SQLAlchemy!
session.execute(
    f"SELECT * FROM users WHERE username = '{username}'"
)

# GOED: raw SQL met parameters in SQLAlchemy
from sqlalchemy import text
session.execute(
    text("SELECT * FROM users WHERE username = :user"),
    {"user": username}
)

3. Least Privilege

De database-gebruiker waarmee je applicatie verbindt, zou zo weinig mogelijk rechten moeten hebben:

-- Maak een beperkte gebruiker
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'sterkwachtwoord';

-- Geef alleen de rechten die nodig zijn
GRANT SELECT, INSERT, UPDATE ON shop.products TO 'webapp'@'localhost';
GRANT SELECT, INSERT ON shop.orders TO 'webapp'@'localhost';

-- NIET dit:
GRANT ALL PRIVILEGES ON *.* TO 'webapp'@'%';
-- ^^^^ dit is het database-equivalent van je voordeur openlaten
--      met een bordje "welkom, neem wat je wilt"

Specifiek: - Geen FILE privilege (voorkomt LOAD_FILE / INTO OUTFILE) - Geen EXECUTE op system stored procedures (voorkomt xp_cmdshell) - Geen superuser/sa-rechten - Beperk tot specifieke tabellen en operaties

4. Web Application Firewall (WAF)

Een WAF is als een uitsmijter bij een club: het houdt de meeste ongewenste bezoekers tegen, maar een vastberaden aanvaller met een net pak komt er toch doorheen.

WAFs detecteren bekende SQL Injection-patronen:

# Deze payloads worden geblokkeerd door de meeste WAFs:
' OR 1=1-- -
' UNION SELECT
; DROP TABLE

# Deze misschien niet:
'/**/OR/**/1=1--/**/-
' /*!50000UNION*/ /*!50000SELECT*/
' uNiOn SeLeCt

Een WAF is een aanvullende verdedigingslaag. Het is geen vervanging voor parameterized queries. Vertrouwen op alleen een WAF is als het dragen van een kogelvrij vest terwijl je de deur van je huis open laat staan.

5. Error handling

Geef nooit database-foutmeldingen door aan de eindgebruiker:

# SLECHT
try:
    cursor.execute(query)
except Exception as e:
    return f"Database error: {e}"  # aanvaller ziet de fout!

# GOED
try:
    cursor.execute(query)
except Exception as e:
    logger.error(f"Database error: {e}")  # log intern
    return "Er is een fout opgetreden."  # generieke melding

Error-based SQL Injection bestaat alleen omdat applicaties hun foutmeldingen tonen. Stop daarmee en een hele klasse aanvallen verdwijnt.

6. Input validatie (als extra laag)

Input validatie is niet je primaire verdediging (dat zijn parameterized queries), maar het is een nuttige extra laag:

import re

def validate_product_id(product_id: str) -> bool:
    """Product ID moet een geheel getal zijn."""
    return bool(re.match(r'^\d+$', product_id))

def validate_sort_column(column: str) -> str:
    """Alleen toegestane kolomnamen voor ORDER BY."""
    allowed = {'name', 'price', 'date', 'rating'}
    if column not in allowed:
        return 'name'  # default
    return column

Let op dat ORDER BY niet geparameteriseerd kan worden (het is een identifier, geen waarde). Gebruik daarvoor een allowlist.

De ongemakkelijke waarheid

Laten we even eerlijk zijn. Het is 2026. SQL Injection is ontdekt in 1998. De eerste grote SQLi-aanval was in 2008 (Heartland Payment Systems, 130 miljoen creditcards). Bobby Tables — de XKCD-strip die SQL Injection uitlegt — is van 2007. Dat is bijna twintig jaar geleden.

En toch. Toch zijn er op dit moment bedrijven die applicaties in productie draaien met code als:

query = "SELECT * FROM users WHERE id = " + request.args.get('id')

Geen parameterized queries. Geen input validatie. Geen WAF. Niets. Nada.

Dit zijn geen startups van twee studenten in een garage. Dit zijn bedrijven met budgetten, met “security teams”, met ISO-certificeringen aan de muur en een CISO die presentaties geeft op conferenties over “het belang van security by design.”

En ergens in de kelder van dat gebouw draait een PHP 5.6-applicatie uit 2009 die de boekhouding doet en die niemand durft aan te raken omdat “hij werkt en er is geen documentatie.” En die applicatie concateneert strings. In 2026. Met directe toegang tot de productiedatabase. Die ook de salarissen bevat.

Dat is geen fout meer. Dat is een keuze. Het is de keuze om de achterdeur open te laten staan en er vervolgens verbaasd over te zijn dat er iemand binnenkomt. Het is de keuze om een rookmelder te kopen, de batterij er niet in te doen, en vervolgens het rookmelderbedrijf aan te klagen als het huis afbrandt.

De patch bestaat. Al meer dan twintig jaar. Het is een enkele regel code. cursor.execute(query, params) in plaats van cursor.execute(f"...{user_input}..."). Het kost vijf minuten om te implementeren. En bedrijven kiezen ervoor om het niet te doen.

Want weet je wat duurder is dan parameterized queries implementeren? Alles behalve parameterized queries implementeren. De pentest die de SQLi vindt. De incident response als het misgaat. De juridische kosten. De boete van de Autoriteit Persoonsgegevens. De PR-kosten om uit te leggen waarom de wachtwoorden van 2 miljoen klanten op Pastebin staan.

Maar die vijf minuten om die ene regel code te veranderen? Nee. Daar is geen budget voor. Dat staat niet in de sprint planning. Dat heeft geen prioriteit.

Escalatiepad-overzicht

Het volgende schema toont hoe een SQL Injection-kwetsbaarheid kan escaleren van een eenvoudige data-lekkage naar volledige server- en domeincontrole:

Cheat sheet: SQL Injection per database

MySQL / MariaDB

Actie Payload
Versie version() of @@version
Huidige database database()
Huidige user current_user() of user()
Alle databases SELECT schema_name FROM information_schema.schemata
Alle tabellen SELECT table_name FROM information_schema.tables WHERE table_schema=database()
Kolommen SELECT column_name FROM information_schema.columns WHERE table_name='X'
String concat concat(), group_concat(), concat_ws()
Substring SUBSTRING(str, pos, len) of MID(str, pos, len)
Comment -- -, #, /* */
Time delay SLEEP(n)
File read LOAD_FILE('path')
File write INTO OUTFILE 'path'
Stacked queries Ja (met PDO/MySQLi multi_query)

PostgreSQL

Actie Payload
Versie version()
Huidige database current_database()
Huidige user current_user of session_user
Alle databases SELECT datname FROM pg_database
Alle tabellen SELECT table_name FROM information_schema.tables WHERE table_schema='public'
String concat string_agg(), || operator
Substring SUBSTRING(str, pos, len)
Comment -- -, /* */
Time delay pg_sleep(n)
File read pg_read_file('path'), lo_import()
File write COPY TO, lo_export()
Command exec COPY FROM PROGRAM 'cmd'
Quote bypass CHR(), $$string$$
Stacked queries Ja

MSSQL

Actie Payload
Versie @@version
Huidige database DB_NAME()
Huidige user SYSTEM_USER of SUSER_SNAME()
Alle databases SELECT name FROM master..sysdatabases
Alle tabellen SELECT name FROM sysobjects WHERE xtype='U'
Kolommen SELECT name FROM syscolumns WHERE id=OBJECT_ID('tabel')
String concat + operator, FOR XML PATH
Substring SUBSTRING(str, pos, len)
Comment -- -, /* */
Time delay WAITFOR DELAY '0:0:n'
Error leakage CONVERT(int, data)
Command exec xp_cmdshell 'cmd'
UNC trigger xp_dirtree '\\ip\share'
Linked server query OPENQUERY("server", 'query')
Stacked queries Ja

Oracle

Actie Payload
Versie SELECT banner FROM v$version
Huidige database SELECT ora_database_name FROM dual
Huidige user SELECT user FROM dual
Alle tabellen SELECT table_name FROM all_tables
Kolommen SELECT column_name FROM all_tab_columns WHERE table_name='X'
String concat || operator
Substring SUBSTR(str, pos, len)
Comment --, /* */
Time delay DBMS_PIPE.RECEIVE_MESSAGE('a', n)
Dummy tabel dual (verplicht bij elke SELECT zonder FROM)
Stacked queries Nee (in de meeste contexten)

Verder lezen

  • OWASP Testing Guide - SQL Injection sectie
  • PortSwigger Web Security Academy - SQL Injection labs (gratis)
  • OWASP SQL Injection Prevention Cheat Sheet - defensieve implementatierichtlijnen
  • PowerUpSQL - https://github.com/NetSPI/PowerUpSQL
  • PayloadsAllTheThings - SQL Injection cheatsheets
  • HackTricks - SQL Injection sectie

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Webbeveiliging ← Home