# Jak zapobiegać SQL injection w PHP

> Jak zapobiegać SQL injection w PHP: prepared statements w PDO i mysqli, białe listy kolumn, skromne uprawnienia bazy. Z przykładami kodu i listą kontrolną.

Źródło: https://phpsos.de/pl/ratgeber/sql-injection-php/  
Stan: 25.10.2026  
Dostawca: PHP SOS, Pomoc awaryjna PHP i bezpieczeństwo serwerów. Natychmiastowa pomoc przez WhatsApp: https://wa.me/491736494914?text=Dzie%C5%84%20dobry%2C%20PHP%20SOS%2C%20potrzebujemy%20pomocy%20z%20naszym%20serwerem%20PHP.%20Firma%3A%20%E2%80%A6%20Strona%20internetowa%3A%20%E2%80%A6

**Krótko:** SQL injection w PHP zapobiegają Państwo niezawodnie, nigdy nie wbudowując wartości w teksty SQL, lecz zawsze przekazując je przez prepared statements z placeholderami. Dla nazw kolumn i tabel, których nie da się przekazać jako placeholder, obowiązuje stała biała lista. Dodatkowo użytkownik bazy danych z minimalnymi uprawnieniami ogranicza szkodę, jeśli jednak pozostanie jakaś luka.

## Dlaczego SQL injection nie może zniknąć z pola widzenia po PHP2Shell

W październiku 2026 wiele krąży wokół PHP2Shell, dwóch krytycznych luk w interpreterze PHP, naprawionych od 21.10.2026 w PHP 8.5.12, 8.4.27, 8.3.36 i 8.2.35. Wiele zespołów zaktualizowało w tych dniach serwery i zrestartowało procesy. To słuszne i pilne.

Aktualny stan PHP nie mówi jednak nic o własnym kodzie. SQL injection od lat należy do najczęstszych podatności w aplikacjach webowych i jest prowadzone w OWASP Top 10 w kategorii Injection. Kto i tak planuje kontrole bezpieczeństwa, powinien od razu przyjrzeć się dostępom do bazy danych we własnej aplikacji.

Dobra wiadomość: SQL injection to jedna z niewielu podatności, które da się niemal całkowicie wykluczyć prostą, konsekwentnie stosowaną zasadą.

## Jak powstaje SQL injection w PHP?

SQL injection powstaje, gdy aplikacja traktuje dane wejściowe użytkownika jako część polecenia SQL zamiast jako czyste dane. Typowe jest składanie zapytania z fragmentów tekstu i zmiennych. Jeśli odwiedzający może określić zawartość takiej zmiennej, może w pewnych okolicznościach zmienić znaczenie polecenia.

Skutki sięgają od odczytania cudzych danych klientów, przez zmianę cen lub uprawnień, po usunięcie całych tabel. W zależności od bazy danych i uprawnień luka może umożliwiać także dostęp wykraczający poza bazę danych. Jeśli dotknięte są dane osobowe, może powstać obowiązek zgłoszenia na podstawie RODO.

Dane wejściowe to nie tylko pola formularzy. Za niegodne zaufania uważa się także parametry URL, ciasteczka, nagłówki HTTP, przesłane pliki, odpowiedzi obcych interfejsów i dane już zapisane we własnej bazie.

## Prepared statements w PDO i mysqli

Prepared statements wysyłają polecenie SQL i wartości do bazy danych oddzielnie. Wartości nigdy nie są interpretowane jako część polecenia. W PDO bezpieczne zapytanie wygląda tak:

```php
<?php
$pdo = new PDO($dsn, $benutzer, $passwort, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES   => false,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, name, email FROM kunden WHERE id = :id AND status = :status');
$stmt->execute([
    'id'     => (int) $_GET['id'],
    'status' => 'aktiv',
]);
$kunde = $stmt->fetch();
```

Opcja `PDO::ATTR_EMULATE_PREPARES => false` sprawia, że baza danych używa prawdziwych prepared statements, o ile sterownik to obsługuje. W mysqli obowiązuje ta sama zasada:

```php
<?php
$stmt = $mysqli->prepare('SELECT id, name FROM produkte WHERE kategorie = ? AND preis <= ?');
$stmt->bind_param('sd', $kategorie, $maxPreis);
$stmt->execute();
$ergebnis = $stmt->get_result();
```

Także listy dla `IN (...)` można odwzorować bezpiecznie, tworząc dla każdej wartości osobny placeholder i przekazując wartości oddzielnie.

```php
<?php
$ids = array_map('intval', $ausgewaehlteIds);
$platzhalter = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, name FROM produkte WHERE id IN ($platzhalter)");
$stmt->execute($ids);
```

## Przypadki szczególne: nazwy kolumn, sortowanie i surowe SQL

Placeholdery działają tylko dla wartości, a nie dla nazw tabel, nazw kolumn czy kierunków sortowania. Właśnie tu w praktyce powstaje wiele luk, na przykład w tabelach z sortowaniem lub filtrach. Rozwiązaniem jest stała lista dozwolonych wartości:

```php
<?php
$erlaubteSpalten = ['name' => 'name', 'preis' => 'preis', 'datum' => 'erstellt_am'];
$spalte   = $erlaubteSpalten[$_GET['sort'] ?? ''] ?? 'name';
$richtung = (($_GET['dir'] ?? '') === 'desc') ? 'DESC' : 'ASC';

$stmt = $pdo->query("SELECT id, name, preis FROM produkte ORDER BY $spalte $richtung");
```

Frameworki i systemy CMS mają własne narzędzia. W WordPressie to zadanie przejmuje `$wpdb->prepare()`, w Laravelu i Symfony query buildery i ORM-y. Gdy tylko używają tam Państwo surowego SQL, na przykład przez funkcje z „raw” w nazwie, znów obowiązują zasady z tego rozdziału. Wskazówki dotyczące Laravela znajdą Państwo na stronie [Zabezpieczanie aplikacji Laravel](https://phpsos.de/pl/plattformen/laravel/).

> **Wskazówka:**
> Proszę celowo szukać w kodzie tekstów SQL, do których wstawiane są zmienne. Prostym początkiem jest wyszukiwanie `query(` i `->raw` w połączeniu z `$_GET`, `$_POST` lub `$_REQUEST`.

## Wyszukiwanie SQL injection w istniejącym kodzie

W nowym oprogramowaniu zasadę można stosować od początku. Trudniej jest w aplikacjach rozwijanych latami. Tam warto systematycznie poszukać miejsc, w których teksty SQL są składane ze zmiennymi:

```bash
# Polecenia SQL, do których wstawiane są zmienne
grep -rnE "(SELECT|INSERT|UPDATE|DELETE)[^;]*\\\$[a-zA-Z_]" src/ --include="*.php" | head -50
# Bezpośrednie użycie danych żądania w funkcjach zapytań
grep -rnE "(query|exec|raw)\s*\(.*\\\$_(GET|POST|REQUEST|COOKIE)" src/ --include="*.php"
```

Nie każde trafienie jest luką, na przykład gdy zmienna zawiera tylko stałą nazwę tabeli. Każde miejsce znalezienia powinno być jednak raz świadomie sprawdzone i w razie potrzeby przestawione na prepared statements. Narzędzia do statycznej analizy kodu pomagają trwale wykrywać takie wzorce w pipeline'ie buildu, tak aby nowe błędy w ogóle nie trafiały na produkcję.

## Ograniczanie szkód: uprawnienia, komunikaty o błędach, kontrola

Bezpieczny kod to pierwsza linia obrony. Dalsze warstwy ograniczają szkodę, jeśli jednak jakaś luka zostanie przeoczona:

- **Minimalne uprawnienia:** użytkownik bazy danych strony potrzebuje zwykle tylko `SELECT`, `INSERT`, `UPDATE` i `DELETE` w swojej bazie, bez uprawnień do zakładania użytkowników czy dostępu do innych baz.
- **Brak komunikatów o błędach na zewnątrz:** `display_errors = Off` w php.ini. Błędy bazy danych należą do logu, a nie do przeglądarki.
- **Rozdzielenie dla każdej aplikacji:** każda strona dostaje osobnego użytkownika bazy danych.
- **Utrzymywanie zależności w aktualnym stanie:** luki tkwią także w bibliotekach, jak pokazuje poradnik [composer audit](https://phpsos.de/pl/ratgeber/composer-abhaengigkeiten-pruefen/).
- **Zlecić kontrolę:** przegląd kodu i zautomatyzowana analiza znajdują wiele miejsc, test penetracyjny pokazuje, co jest faktycznie osiągalne z zewnątrz.

```sql
-- Przykład: użytkownik bazy danych tylko z niezbędnymi uprawnieniami
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY '<starkes Passwort>';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
```

Pokrewna podatność dotyczy wyświetlania danych w przeglądarce. Jak ją zabezpieczyć, wyjaśnia poradnik [Jak zapobiegać cross-site scripting w PHP](https://phpsos.de/pl/ratgeber/xss-php/).

## Jak konkretnie pomaga PHP SOS?

- Systematycznie sprawdzamy dostępy do bazy danych Państwa aplikacji pod kątem niebezpiecznych zapytań.
- Priorytetyzujemy znaleziska według osiągalności i dotkniętych danych oraz pokazujemy konkretne poprawki.
- Ustawiamy uprawnienia bazy danych i obsługę błędów na bezpiecznym poziomie.
- Po poprawce testujemy ponownie i dokumentujemy wynik.

Niezależną kontrolę opisuje usługa [Audyt bezpieczeństwa i test penetracyjny](https://phpsos.de/pl/leistungen/sicherheitsaudit-penetrationstest/).


## Częste pytania

**Czy wystarczy obrabiać dane wejściowe przez addslashes lub real_escape_string?**  
Nie, to podatne na błędy. Escapowanie zależy od zestawu znaków i kontekstu i łatwo je w jednym miejscu pominąć. Prepared statements zasadniczo rozdzielają polecenie i dane, dlatego są zalecaną drogą.

**Czy ORM automatycznie chroni przed SQL injection?**  
W dużej mierze, dopóki korzystają Państwo z funkcji zapytań ORM. Gdy tylko piszą Państwo surowe SQL lub wstawiają wartości do sortowania i nazw kolumn, obowiązują te same zasady co bez ORM.

**Czy zapora aplikacji webowych (WAF) pomaga przeciw SQL injection?**  
WAF może przechwycić znane wzorce i zyskać czas. Nie zastępuje jednak bezpiecznego kodu, ponieważ reguły oparte na danych żądania można obejść, a czasem trafiają w uprawnione dane wejściowe.

**Czy aktualizacja PHP przeciw PHP2Shell zamyka także luki SQL injection?**  
Nie. PHP2Shell dotyczy interpretera PHP. SQL injection to błąd w kodzie aplikacji i pozostaje, niezależnie od tego, jaka wersja PHP działa.

## Źródła

- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
- [php.net: komunikaty o bezpieczeństwie](https://www.php.net/)
