SQL-Injection verstehen und zuverlässig verhindern
OWASP
0x00
Security · SQL-Injection · OWASP Top 10 · Magento 2
SQL-Injection verstehen und zuverlässig verhindern
Von String-Konkatenation zu sicheren Prepared Statements

SQL-Injection zählt seit Jahren zu den gefährlichsten Schwachstellen in Webanwendungen und ermöglicht Angreifern das Auslesen, Verändern oder Löschen kompletter Datenbanken über manipulierte Eingabefelder. Dieser Artikel erklärt die Mechanik von klassischer und blinder SQL-Injection, zeigt warum Escaping allein nicht ausreicht und liefert praxisnahe Lösungen mit Prepared Statements, sicheren ORM-Mustern und der Magento ResourceConnection API.

16 Min. Lesezeit Prepared Statements · ResourceConnection · Blind Injection Magento 2.4.8 · PHP 8.4 · PDO

1. Was SQL-Injection so gefährlich macht

SQL-Injection gehört seit über zwei Jahrzehnten zu den OWASP Top 10 und steht dort aktuell unter der Kategorie A03:2021 Injection weiterhin ganz oben. Der Grund ist einfach: Eine erfolgreiche SQL-Injection gibt einem Angreifer nicht nur Lesezugriff auf einzelne Datensätze, sondern potenziell auf die komplette Datenbank, oft inklusive Kundendaten, Passwort-Hashes und Zahlungsinformationen, ohne dass eine gültige Authentifizierung nötig wäre. In vielen Fällen lässt sich über UNION-based Injection oder gespeicherte Prozeduren sogar Schreibzugriff erlangen, Daten manipulieren oder über Funktionen wie INTO OUTFILE Dateien auf dem Server ablegen, was den Weg zu Remote Code Execution öffnen kann.

Dass SQL-Injection trotz jahrzehntelanger Bekanntheit weiterhin regelmäßig in Sicherheitsvorfällen auftaucht, liegt selten an fehlendem Wissen, sondern an konkreten Codestellen: Legacy-Skripte, die vor der breiten Nutzung von PDO geschrieben wurden, Custom-Module mit eigenen SQL-Statements für Reports oder Exporte, und ORM-Fallbacks wie whereRaw() oder query(), die Entwickler unter Zeitdruck mit String-Konkatenation statt Parametern befüllen. Gerade in Magento-Projekten mit vielen Drittanbieter-Modulen und individuellen Erweiterungen reicht eine einzige ungeprüfte Codestelle mit direkter String-Verkettung, um den gesamten Shop inklusive Kundendatenbank zu kompromittieren.

2. Wie SQL-Injection mechanisch funktioniert

Der Mechanismus hinter SQL-Injection ist strukturell simpel: Eine Anwendung baut einen SQL-Query-String zusammen, indem sie Nutzereingaben direkt in die Zeichenkette einfügt, statt sie als separate Daten zu behandeln. Ein Query wie SELECT * FROM customer WHERE email = '" . $_POST['email'] . "' erwartet, dass die Eingabe eine harmlose E-Mail-Adresse ist. Sendet ein Angreifer stattdessen den String ' OR '1'='1, entsteht nach der Konkatenation die Bedingung WHERE email = '' OR '1'='1', die für jede Zeile der Tabelle wahr ist. Die Anfrage gibt plötzlich alle Kundendatensätze zurück, nicht nur den mit der angegebenen E-Mail-Adresse.

Entscheidend ist, dass der Angreifer nicht nur Bedingungen verändert, sondern die komplette Query-Syntax kontrolliert. Mit einem schließenden Anführungszeichen, gefolgt von einem Kommentarzeichen wie -- oder #, lässt sich der Rest der ursprünglichen Query auskommentieren, etwaige nachfolgende Bedingungen wie AND active = 1 greifen dann nicht mehr. Manche Datenbanktreiber erlauben zusätzlich Stacked Queries, bei denen ein Semikolon eine zweite, komplett eigene Anweisung einleitet, zum Beispiel ein DROP TABLE. Ob Stacked Queries funktionieren, hängt vom verwendeten Treiber ab: PDO mit MySQL erlaubt sie standardmäßig nicht, PHPs mysqli in bestimmten Konfigurationen jedoch schon.

3. Klassische, Union-based und Blind/Time-based Injection

Bei klassischer, error-based oder UNION-based SQL-Injection sieht der Angreifer die Auswirkung seiner Eingabe direkt in der Antwort der Anwendung: Eine Fehlermeldung verrät die Datenbankstruktur, oder ein UNION SELECT hängt zusätzliche Spalten aus einer beliebigen Tabelle an das reguläre Ergebnis an und zeigt sie im Frontend an, etwa in einer Produktliste oder Suchergebnisseite. Voraussetzung dafür ist, dass Anzahl und Datentyp der Spalten im UNION SELECT zur ursprünglichen Query passen, was Angreifer durch systematisches Ausprobieren mit ORDER BY schnell herausfinden.

Blind SQL-Injection kommt zum Einsatz, wenn die Anwendung keine Datenbankfehler oder Rohdaten anzeigt. Bei Boolean-based Blind Injection sendet der Angreifer Bedingungen wie AND 1=1 und AND 1=2 und beobachtet, ob sich die Antwort unterscheidet, etwa ein sichtbares Produkt versus eine leere Seite. Noch subtiler ist Time-based Blind Injection: Mit einer Bedingung wie AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) wird die Antwortzeit zum Signal. Reagiert der Server fünf Sekunden verzögert, ist die Bedingung wahr. Zeichen für Zeichen lässt sich so ein komplettes Passwort-Hash extrahieren, auch wenn die Anwendung nach außen keinerlei sichtbaren Unterschied zeigt.


-- Original application query (built via string concatenation)
-- SELECT id, name, price FROM product WHERE category_id = <input>

-- 1. UNION-based: append attacker-chosen columns to the result set
1 UNION SELECT username, password_hash, NULL FROM admin_user --

-- 2. Boolean-based blind: compare responses for true vs. false conditions
1 AND 1=1   -- page renders normally
1 AND 1=2   -- page renders empty or different

-- 3. Time-based blind: infer data one character at a time via delay
1 AND IF(SUBSTRING((SELECT password_hash FROM admin_user LIMIT 1),1,1)='a', SLEEP(5), 0)

4. Prepared Statements und parametrisierte Queries als echte Lösung

Der einzig strukturell zuverlässige Schutz vor SQL-Injection sind Prepared Statements mit parametrisierten Queries. Der entscheidende Unterschied zu String-Konkatenation liegt im Ablauf: Die Anwendung schickt zunächst nur die Query-Struktur mit Platzhaltern an die Datenbank, etwa SELECT * FROM customer WHERE email = ?. Der Datenbanktreiber parst und kompiliert diese Struktur vollständig, bevor überhaupt ein einziger Nutzerwert übertragen wird. Erst danach werden die eigentlichen Werte in einem separaten Schritt über das Binärprotokoll gesendet und an die bereits kompilierte Query gebunden.

Weil die Werte den SQL-Parser nie erreichen, können sie die Query-Struktur grundsätzlich nicht mehr verändern. Ein Anführungszeichen oder ein Kommentarzeichen im Eingabewert bleibt einfach Teil der Zeichenkette, egal wie es aussieht. Das unterscheidet Prepared Statements fundamental von Escaping-Funktionen, die versuchen, gefährliche Zeichen im Voraus unschädlich zu machen, aber immer im selben String-Kontext bleiben, den auch der Angreifer kontrolliert. In PHP übernimmt PDO diese Trennung mit bindParam() oder direkt beim Aufruf von execute() mit einem Werte-Array, ohne dass Entwickler manuell escapen müssen.


<?php
declare(strict_types=1);

// VULNERABLE: user input concatenated directly into the SQL string
function findCustomerByEmailUnsafe(PDO $pdo, string $email): array|false
{
    // An attacker sending "' OR '1'='1" turns this into a query
    // that matches every row in the table.
    $sql = "SELECT * FROM customer WHERE email = '" . $email . "'";
    $result = $pdo->query($sql);

    return $result->fetch(PDO::FETCH_ASSOC);
}

// SECURE: prepared statement, value bound as data, not SQL
function findCustomerByEmailSafe(PDO $pdo, string $email): array|false
{
    $stmt = $pdo->prepare('SELECT * FROM customer WHERE email = :email');
    $stmt->execute(['email' => $email]);

    return $stmt->fetch(PDO::FETCH_ASSOC);
}

5. Warum Escaping allein nicht ausreicht

Escaping-Funktionen wie mysqli_real_escape_string() oder addslashes() galten lange als ausreichender Schutz, sind aber strukturell unsicherer als Prepared Statements. Sie versuchen, Sonderzeichen wie Anführungszeichen im Voraus zu maskieren, arbeiten aber weiterhin im selben String, den die Anwendung anschließend per Konkatenation in die Query einbaut. Das eröffnet Umgehungsmöglichkeiten, die mit dem Query-Kontext zusammenhängen: In numerischen Kontexten ohne umschließende Anführungszeichen, etwa WHERE id = " . $id, hilft Escaping überhaupt nicht, weil kein Anführungszeichen zum Escapen vorhanden ist. Ein Wert wie 1 OR 1=1 funktioniert unverändert.

Historisch bekannt ist außerdem der Bypass über Multi-Byte-Zeichensätze: Bei falsch konfiguriertem Zeichensatz wie GBK konnte ein vorangestelltes Backslash-Escapezeichen mit dem folgenden Byte ein gültiges Mehrbyte-Zeichen bilden, wodurch das eigentliche Escapezeichen im resultierenden String verschwand und das folgende Anführungszeichen wieder aktiv wurde. Solche Bugs zeigen, dass Escaping von der korrekten Zeichensatz-Konfiguration der Verbindung abhängt und damit eine zusätzliche Fehlerquelle darstellt, die bei Prepared Statements strukturell gar nicht existiert, weil Werte nie in einen String-Kontext eingebettet werden.

6. ORM- und Query-Builder-Sicherheit

Moderne ORMs wie Doctrine oder Query Builder wie der von Laravel nutzen intern standardmäßig parametrisierte Queries und sind bei normaler Verwendung sicher gegen SQL-Injection. Ein Aufruf wie $qb->where('email = :email')->setParameter('email', $input) bindet den Wert exakt wie ein manuelles Prepared Statement. Das Sicherheitsversprechen eines ORMs gilt jedoch nur für den Standardpfad, nicht für jede API-Methode, die das ORM anbietet, und genau dort entstehen in der Praxis die meisten Lücken.

Methoden wie whereRaw(), DB::statement(), direkt eingebettete Strings oder orderBy() mit einem aus der Request stammenden Spaltennamen umgehen die Parametrisierung komplett, sobald ein Entwickler Nutzereingaben per String-Interpolation einfügt. Besonders tückisch sind ORDER-BY- und Tabellennamen, weil sich Spalten- und Tabellennamen grundsätzlich nicht als Bind-Parameter übergeben lassen. Datenbanktreiber erlauben Parameter nur für Werte, nicht für Identifier. Der einzig sichere Weg dafür ist eine Whitelist erlaubter Spaltennamen, gegen die die Nutzereingabe geprüft wird, bevor sie in die Query eingesetzt wird.

7. Magento ResourceConnection und Select API sicher nutzen

Magento kapselt Datenbankzugriffe über \Magento\Framework\App\ResourceConnection und stellt mit getConnection()->select() eine Zend_Db_Select-kompatible API bereit, die Platzhalter und Bindings nativ unterstützt. Die Methode where() akzeptiert ein ?-Platzhalter-Pattern und bindet Werte automatisch sicher, zum Beispiel $select->where('email = ?', $email). Für komplexere Fälle mit dynamisch zusammengesetzten Bedingungen bietet die Connection zusätzlich quoteInto(), das einen Wert korrekt maskiert und in einen Bedingungsstring einsetzt, ohne dass Entwickler manuell escapen müssen.

In Custom-Modulen ist der kritische Fehler nahezu immer derselbe: Ein Entwickler ruft $connection->query() direkt mit einem konkatenierten String auf, statt die Select-API oder gebundene Parameter zu nutzen. Auch bei Collections ist Vorsicht geboten: addFieldToFilter('entity_id', $id) mit einem direkt übergebenen Integer aus der Request ist durch die interne Zend_Db-Bindung meist sicher, aber die explizite Array-Form addFieldToFilter('entity_id', ['eq' => $id]) macht die Absicht eindeutig und verhindert Fehlinterpretationen bei komplexeren Bedingungen wie ['in' => $ids] oder ['like' => $term].


<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Model;

use Magento\Framework\App\ResourceConnection;

/**
 * Reads product data by SKU using safe, parameterized queries.
 */
class ProductLookup
{
    /**
     * @param ResourceConnection $resourceConnection Magento database connection resolver.
     */
    public function __construct(
        private readonly ResourceConnection $resourceConnection
    ) {
    }

    /**
     * VULNERABLE: raw string concatenation into a query.
     *
     * @param string $sku Product SKU coming from user input.
     * @return array<string, mixed>|false
     */
    public function findBySkuUnsafe(string $sku): array|false
    {
        $connection = $this->resourceConnection->getConnection();
        // Never do this: $sku could contain "' OR '1'='1"
        $query = "SELECT * FROM catalog_product_entity WHERE sku = '" . $sku . "'";

        return $connection->fetchRow($query);
    }

    /**
     * SECURE: Select API with a bound placeholder.
     *
     * @param string $sku Product SKU coming from user input.
     * @return array<string, mixed>|false
     */
    public function findBySkuSafe(string $sku): array|false
    {
        $connection = $this->resourceConnection->getConnection();
        $table = $this->resourceConnection->getTableName('catalog_product_entity');

        $select = $connection->select()
            ->from($table)
            ->where('sku = ?', $sku);

        return $connection->fetchRow($select);
    }
}

8. Typische Fallstricke in Magento-Modulen und Defense in Depth

Typische Einfallstore in Magento-Projekten liegen selten im Core-Code, sondern in individuellen Erweiterungen: Setup-Skripte und Data-Patches, die während der Migration mit rohen query()-Aufrufen Massendaten verarbeiten, benutzerdefinierte Admin-Grids mit eigenen Sortier- und Filterlogiken, sowie Reporting-Module, die Spaltennamen oder Sortierrichtung direkt aus GET-Parametern übernehmen. Da sich Spaltennamen wie erwähnt nicht parametrisieren lassen, braucht jede ORDER-BY-Logik, die auf Nutzereingaben reagiert, eine feste Whitelist erlaubter Werte, gegen die validiert wird, bevor der Wert in die Query gelangt.

Prepared Statements sind die primäre Verteidigungslinie, sollten aber durch weitere Schichten ergänzt werden. Der Datenbank-User, mit dem sich die Magento-Anwendung verbindet, sollte nur die tatsächlich benötigten Rechte besitzen: kein FILE-Privileg für INTO OUTFILE, kein DROP auf Produktionstabellen und kein Zugriff auf systemnahe Datenbanken. Eine Web Application Firewall kann bekannte Injection-Muster wie UNION SELECT oder SLEEP() vor der Anwendung abfangen, ersetzt aber niemals sichere Query-Konstruktion im Code, sondern reduziert nur das Zeitfenster bis ein Patch verfügbar ist.

Ergänzend hilft kontinuierliches Logging von Datenbank-Fehlern und ungewöhnlich langsamen Queries, da Time-based Blind Injection durch künstliche Verzögerungen auffällt, sobald man Ausreißer in der Query-Laufzeit systematisch auswertet. Ein einfaches Audit-Skript, das den Code regelmäßig nach gefährlichen Mustern durchsucht, ergänzt automatisierte statische Analyse und manuelle Code-Reviews vor jedem Release.


#!/usr/bin/env bash
# audit-sql-concat.sh - find likely unsafe SQL concatenation in the codebase
# Run before every release as part of the security checklist.
set -euo pipefail

echo "[*] Scanning app/code for raw query() calls with string concatenation..."
grep -rnE "->query\(.*\.\s*\\\$" app/code --include="*.php" || true

echo "[*] Scanning for direct variable interpolation in SQL strings..."
grep -rnE "SELECT .*\\\$[a-zA-Z_]+" app/code --include="*.php" || true

echo "[*] Scanning for ORDER BY built from request parameters..."
grep -rnE "ORDER BY.*\\\$_(GET|POST|REQUEST)" app/code --include="*.php" || true

echo "[*] Done. Review every match manually before deploying."

{
  "event": "suspicious_query_detected",
  "severity": "high",
  "timestamp": "2026-07-12T09:14:32Z",
  "source_ip": "203.0.113.42",
  "endpoint": "/catalogsearch/result",
  "signal": "time_based_delay",
  "query_duration_ms": 5023,
  "matched_pattern": "SLEEP(",
  "action_taken": "request_blocked",
  "waf_rule_id": "942100"
}

9. SQL-Injection-Schutzmaßnahmen im Vergleich

Die folgende Übersicht fasst die häufigsten unsicheren Muster aus Magento- und PHP-Projekten den jeweils sicheren Alternativen gegenüber, inklusive der technischen Begründung, warum das sichere Pattern strukturell wirkt und nicht nur zufällig funktioniert.

Angriffsfläche Unsicheres Muster Sicheres Pattern Warum sicher
Query mit Nutzereingabe "...WHERE email='".$_POST['email']."'" PDO::prepare() + bindParam() Werte erreichen den SQL-Parser nie
Eingabe maskieren mysqli_real_escape_string() Parametrisierte Query Kein String-Kontext, keine Zeichensatz-Bypässe
Sortierung aus Request "ORDER BY " . $_GET['sort'] Whitelist erlaubter Spaltennamen Identifier lassen sich nicht binden
Magento Custom Query $connection->query() mit Konkatenation select()->where('col = ?', $val) Bindings über ResourceConnection API
Datenbank-Rechte App-User mit vollen Rechten App-User mit minimalen Grants Begrenzt Schaden bei erfolgreicher Injection

Kein einzelnes Pattern aus der Tabelle ersetzt die anderen vollständig. Prepared Statements verhindern die Injection selbst, Whitelisting sichert die Stellen ab, die sich grundsätzlich nicht parametrisieren lassen, und minimale Datenbankrechte begrenzen den Schaden, falls eine Schwachstelle trotzdem übersehen wird. Erst das Zusammenspiel aller drei Ebenen macht eine Magento-Anwendung strukturell robust gegen SQL-Injection.

Mironsoft

Security-Audits, Code-Reviews und Härtung für Magento- und Hyvä-Shops

Bereit für ein Security-Audit eurer Datenbankzugriffe?

Wir durchsuchen euren Magento-Code systematisch nach unsicheren Query-Mustern, ersetzen String-Konkatenation durch Prepared Statements und ResourceConnection-Bindings und richten Monitoring für verdächtige Datenbankzugriffe ein.

Code-Audit

Systematische Suche nach unsicheren Query-Mustern und Injection-Risiken

Refactoring

Prepared Statements, ResourceConnection-Bindings und Least-Privilege-DB-User

Monitoring-Setup

Query-Logging, WAF-Regeln und Alerting bei verdächtigen Zugriffen

10. Zusammenfassung

SQL-Injection entsteht immer aus demselben Grundfehler: Nutzereingaben werden als Teil der SQL-Syntax behandelt statt als reine Daten. Klassische, Union-based und Blind-Varianten unterscheiden sich nur darin, wie ein Angreifer das Ergebnis ausliest, nicht in der zugrunde liegenden Schwachstelle. Prepared Statements mit parametrisierten Queries lösen das Problem strukturell, weil Werte den SQL-Parser nie erreichen und die Query-Struktur damit unabhängig vom Eingabeinhalt fest steht. Escaping-Funktionen bleiben demgegenüber im selben String-Kontext und sind anfällig für Zeichensatz-Bypässe und Kontextfehler.

In Magento-Projekten heißt das konkret: ResourceConnection und die Select-API mit ?-Platzhaltern und quoteInto() statt query() mit String-Konkatenation, eine feste Whitelist für Spaltennamen bei dynamischem ORDER BY, und ein Datenbank-User mit minimalen Rechten als letzte Verteidigungslinie, falls eine Schwachstelle trotzdem übersehen wird. Wer diese Muster konsequent in Code-Reviews und automatisierten Audits durchsetzt, schließt eine der häufigsten und gleichzeitig am einfachsten vermeidbaren Schwachstellenklassen der OWASP Top 10 dauerhaft.

SQL-Injection verstehen und verhindern - Das Wichtigste auf einen Blick

Prepared Statements

PDO/parametrisierte Queries binden Werte getrennt vom SQL-Parser. Der einzige strukturell zuverlässige Schutz vor SQL-Injection.

Escaping reicht nicht

mysqli_real_escape_string() & Co bleiben im selben String-Kontext und sind von der Zeichensatz-Konfiguration abhängig.

Magento ResourceConnection

select()->where('col = ?', $val) und quoteInto() statt query() mit Konkatenation nutzen.

Defense in Depth

Least-Privilege-DB-User, Whitelisting für ORDER BY/Identifier, WAF-Regeln und Query-Monitoring ergänzen sichere Queries.

11. FAQ: SQL-Injection verstehen und verhindern

1Was ist SQL-Injection?
Eine Schwachstelle, bei der unvalidierte Nutzereingaben als Teil einer SQL-Query interpretiert werden und die Abfragelogik verändern. Angreifer erhalten so potenziell Lese- und Schreibzugriff auf die komplette Datenbank.
2Wie funktioniert eine klassische SQL-Injection technisch?
Die Anwendung baut Queries per String-Konkatenation. Ein Anführungszeichen im Eingabewert beendet die erwartete Zeichenkette vorzeitig, der Rest wird als eigener SQL-Code interpretiert.
3Was ist Blind SQL-Injection?
Wenn keine Datenbankfehler oder Rohdaten angezeigt werden. Der Angreifer leitet die Antwort aus Verhaltensunterschieden ab, etwa AND 1=1 versus AND 1=2.
4Was ist Time-based Blind SQL-Injection?
Eine Bedingung löst bei Wahrheit eine künstliche Verzögerung wie SLEEP(5) aus. Über die Antwortzeit lässt sich Zeichen für Zeichen Dateninhalt extrahieren.
5Warum reicht Escaping nicht aus?
Escaping bleibt im selben String-Kontext. In numerischen Kontexten ohne Anführungszeichen wirkt es gar nicht, und bei falscher Zeichensatz-Konfiguration lässt es sich historisch umgehen.
6Was sind Prepared Statements und warum sind sie sicher?
Sie trennen Query-Struktur und Werte. Die Struktur wird zuerst kompiliert, Werte werden erst danach gebunden und erreichen den SQL-Parser nie.
7Ist ein ORM automatisch sicher vor SQL-Injection?
Der Standardpfad ist meist sicher. Methoden wie whereRaw() oder query() mit String-Interpolation umgehen den Schutz jedoch vollständig.
8Wie schützt man Magento-ResourceConnection-Zugriffe konkret?
Select-API mit ?-Platzhaltern, quoteInto() für dynamische Bedingungen, und die Array-Form von addFieldToFilter() statt roher query()-Aufrufe.
9Warum braucht der Datenbank-User minimale Rechte?
Selbst bei übersehener Schwachstelle begrenzt ein App-User ohne FILE-, DROP- oder Admin-Rechte den Schaden erheblich. Zusätzliche Schicht, kein Ersatz für sichere Queries.
10Wie erkennt man SQL-Injection-Versuche im laufenden Betrieb?
Monitoring auffällig langsamer Queries, WAF-Regeln für bekannte Muster wie UNION SELECT oder SLEEP(), und Logging von Datenbank-Fehlern.