Przejdź do głównej zawartości

SQL Injection - jak działa i jak się bronic

SQL Injection (SQLi) to jeden z najstarszych, ale wciaz najgrozniejszych atakow na aplikacje webowe. Polega na wstrzyknieciu złośliwego kodu SQL do zapytania bazodanowego poprzez dane wejsciowe użytkownika. Skuteczny atak SQLi może prowadzic do wycieku całej bazy danych, modyfikacji lub usuniecia danych, a nawet przejecia kontroli nad serwerem.

Według OWASP, SQL Injection od lat zajmuje czołowe miejsca wsrod najczestszych podatnosci aplikacji webowych, mimo że metody ochrony sa dobrze znane i łatwe do wdrozenia.

In-band SQLi (Classic)

Atakujacy używa tego samego kanału do przeprowadzenia ataku i odczytu wynikow. Najprostsza i najczestsza forma.

Blind SQLi

Wyniki nie sa bezposrednio widoczne. Atakujacy wnioskuje o strukturze bazy na podstawie zachowania aplikacji (Boolean-based lub Time-based).

Out-of-band SQLi

Dane sa eksfiltrowane przez inny kanał (np. DNS, HTTP). Stosowany gdy inne metody zawodza.

SkutekOpis
Wyciek danychKradziez loginow, haseł, danych osobowych, kart kredytowych
Modyfikacja danychZmiana sald, uprawnien, statusow zamowien
Usuniecie danychDROP TABLE, DELETE - utrata danych
Obejscie autentykacjiLogowanie bez znajomosci hasła
Przejecie serweraW skrajnych przypadkach - wykonanie komend systemowych
Szkody wizerunkoweUtrata zaufania klientow, kary RODO
  1. Aplikacja buduje zapytanie SQL z danymi użytkownika

    $query = "SELECT * FROM users WHERE username = '$username'";
  2. Atakujacy wprowadza złośliwe dane

    Zamiast normalnej nazwy użytkownika wpisuje:

    admin' OR '1'='1
  3. Powstaje zmodyfikowane zapytanie

    SELECT * FROM users WHERE username = 'admin' OR '1'='1'
  4. Warunek ‘1’=‘1’ jest zawsze prawdziwy

    Zapytanie zwraca wszystkich użytkowników lub omija autentykacje.

Formularz logowania:

<form method="POST">
<input name="username" placeholder="Login" />
<input name="password" type="password" placeholder="Hasło" />
<button type="submit">Zaloguj</button>
</form>

Podatny kod PHP:

<?php
$username = $_POST['username'];
$password = $_POST['password'];
// NIEBEZPIECZNE - konkatenacja stringow
$query = "SELECT * FROM users
WHERE username = '$username'
AND password = '$password'";
$result = $db->query($query);
if ($result->rowCount() > 0) {
echo "Zalogowano!";
}

Atak - w polu username wpisujemy:

admin'--

Wynikowe zapytanie:

SELECT * FROM users WHERE username = 'admin'--' AND password = ''

Znaki -- komentuja reszte zapytania, wiec warunek hasła jest ignorowany.

-- Union-based: odczyt danych z innej tabeli
' UNION SELECT username, password FROM users--
-- Stacked queries: wykonanie dodatkowego zapytania
'; DROP TABLE users;--
-- Time-based blind: wnioskowanie przez opoznienie
' OR IF(1=1, SLEEP(5), 0)--
-- Boolean-based blind: wnioskowanie przez różna odpowiedz
' AND 1=1-- (zwraca dane)
' AND 1=2-- (nie zwraca danych)
-- Error-based: wyciaganie danych przez komunikaty błędów
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT password FROM users LIMIT 1)))--
<?php
// NIEBEZPIECZNE
$search = $_GET['q'];
$query = "SELECT * FROM products WHERE name LIKE '%$search%'";
$products = $db->query($query);

Atak:

%' UNION SELECT id, username, password, email FROM users--
<?php
// BEZPIECZNE - Prepared Statements z PDO
$search = $_GET['q'];
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE :search");
$stmt->execute(['search' => '%' . $search . '%']);
$products = $stmt->fetchAll();
<?php
// BEZPIECZNE
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
// Weryfikacja hasła osobno (z użyciem password_verify dla hashowanych haseł)
if ($user && password_verify($password, $user['password_hash'])) {
echo "Zalogowano!";
$_SESSION['user_id'] = $user['id'];
} else {
echo "Nieprawidłowe dane logowania";
}
<?php
// BEZPIECZNE - MySQLi prepared statements
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId); // "i" = integer, "s" = string
$stmt->execute();
$result = $stmt->get_result();
// BEZPIECZNE - parametryzowane zapytanie
const mysql = require('mysql2/promise');
async function getUser(userId) {
const connection = await mysql.createConnection({/* config */});
// Znak ? jest placeholderem
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return rows[0];
}
CREATE PROCEDURE GetUser(IN userId INT)
BEGIN
SELECT * FROM users WHERE id = userId;
END
$stmt = $pdo->prepare("CALL GetUser(:id)");
$stmt->execute(['id' => $userId]);
<?php
// Dodatkowa warstwa - walidacja typu
$userId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($userId === false) {
die('Nieprawidłowe ID');
}
// Whitelist dla dozwolonych wartości
$allowedSort = ['name', 'date', 'price'];
$sort = in_array($_GET['sort'], $allowedSort) ? $_GET['sort'] : 'name';
-- Użytkownik aplikacji nie powinien mieć pełnych uprawnien
GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'localhost';
-- Bez: DROP, DELETE, ALTER, CREATE
// Doctrine ORM - automatyczna ochrona
$user = $entityManager->find(User::class, $userId);
// QueryBuilder
$qb = $entityManager->createQueryBuilder();
$qb->select('u')
->from(User::class, 'u')
->where('u.email = :email')
->setParameter('email', $email);

SQL Injection pozostaje jednym z najgroźniejszych atakow mimo prostych metod ochrony. Kluczowe zasady:

  1. ZAWSZE uzywaj Prepared Statements - bez wyjatkow
  2. NIGDY nie konkatenuj danych użytkownika z SQL
  3. Waliduj dane wejsciowe jako dodatkowa warstwe
  4. Stosuj zasade najmniejszych uprawnien dla konta DB
  5. Uzywaj ORM gdy to możliwe - automatyczna ochrona

Pamiętaj: Jeden podatny endpoint może prowadzic do wycieku całej bazy danych. Koszt wdrozenia prepared statements jest minimalny, a konsekwencje zaniedbania - ogromne.