Za řešení odpovídající úrovni dnešní doby se považuje oddělené předávání dotazu a jeho parametrů databázovému serveru. Nejmodernější obrana proti SQL injection (injekci) jsou tedy parametrizované dotazy v PHP. Jde to v PHP s (již) objektovým rozhraním MySQLi i PDO. Někdy se pro to používá termín „svazování proměnných“ nebo SQL injekce, injektáž. Zde jsou příklady ochrany proti SQL injection (injekci) v rozhraní MySQLi.
phpmyadmin
O napadení webu a zjištění hesel útočníky už toho bylo napsáno hodně. Dříve se doporučovalo escapovat nebo používat mysqli->real_escape_string() a na čísla intval(n) nebo floatval(n.nn). Navíc hesla do databáze ukládat jako hashe SHA512 a mít sloupec sůl. Před uložením hesla do databáze toto heslo ještě „osolit“ jedinečnou solí pro každý záznam.
To je sice zatím bezpečné, ale má to háček. Tím je lidský faktor a zapomenutí escapovat proměnné nebo ošetřit čísla vstupující do SQL dotazu z formuláře. Pak se stane webová stránka zranitelná na SQL injekci (SQL injection).
Proč používat parametrizovaný dotaz? Nejdůležitějším důvodem pro použití parametrizovaných dotazů je vyhnout se útokům s vložením SQL injekce (injection). Je to ve své podstatě změna SQL dotazu odeslanými daty (většinou z formuláře). Lidský faktor je chybující prvek, zapomenutí escapovat nějakou proměnnou může mít fatální následky. Lidé prostě dělají chyby, stroje ne. Za druhé, parametrizovaný dotaz se postará o scénář, kde by sql dotaz mohl selhat, např. vložení O'Baily do SQL dotazu.
Proto se nyní (rok 2022) doporučuje používat v PHP parametrizované SQL dotazy a ukládat hesla do databáze pomocí PHP funkce password_hash(). Ověřovat heslo pomocí sesterské funkce password_verify(). Více informací je v článku „bezpečné ukládání hesel do MariaDB“. Jako PHP rozhraní pro práci s MariaDB lze používat objektové PDO nebo již taky objektové MYSQLI.
Nejdříve si vytvoříme účet testik, databázi testik a tabulku uživatele v utf8 pomocí phpmysqladmin
(https://localhost/phpmyadmin/).
MariaDB klient, monitor, konzola - psaní SQL dotazů v příkazovém řádku, status - zobrazení nastavení znakovách sad v databázi MariaDB
virhundo@hirunduv:~$ mariadb -u testik -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 151
Server version: 10.3.34-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> \C utf8mb4
Charset changed
MariaDB [(none)]> status
--------------
mariadb Ver 15.1 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
Connection id: 401
Current database:
Current user: dtbz_nasstenka@localhost
SSL: Not in use
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server: MariaDB
Server version: 10.6.18-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04
Protocol version: 10
Connection: Localhost via UNIX socket
Server characterset: utf8mb4
Db characterset: utf8mb4
Client characterset: utf8mb4
Conn. characterset: utf8mb4
UNIX socket: /run/mysqld/mysqld.sock
Uptime: 8 hours 1 min 8 sec
Threads: 1 Questions: 2218 Slow queries: 1 Opens: 227 Open tables: 220 Queries per second avg: 0.076
--------------
MariaDB [(none)]> use testik;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [testik]> show tables;
+------------------+
| Tables_in_testik |
+------------------+
| uzivatele |
+------------------+
1 row in set (0.001 sec)
MariaDB [testik]> describe uzivatele;
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| uid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| ucet | varchar(15) | NO | UNI | NULL | |
| heslo | varchar(128) | NO | | NULL | |
| prijm | varchar(25) | NO | | NULL | |
| jmeno | varchar(25) | NO | | NULL | |
+-------+------------------+------+-----+---------+----------------+
5 rows in set (0.020 sec)
MariaDB [testik]> select * from uzivatele;
+-----+---------------+--------------------------------------------------------------+---------------+-----------+
| uid | ucet | heslo | prijm | jmeno |
+-----+---------------+--------------------------------------------------------------+---------------+-----------+
| 1 | admin | $2y$10$Zus3PC71xQit/2Bbu/esRO9nUGHHiUCyUASnPkO6mX3Ml0IngGtEm | Světlá | Karolína |
| 2 | šéfredaktor | $2y$10$/mL7eGXPDTVHc9l5x/iVZeIowmOaDJg4th8UkcG21.DhxxZrdudyW | Pan | Tau |
| 3 | redaktor | $2y$10$jiIi1oAy6Q4BQVuCZy2k/OWIEa66/xkqh450T.4kE78DQ7smViN4y | Novák | Petr |
| 4 | přispěvatel | $2y$10$QmCpFjBgoV93mu9IeT0UKOoGt6NMMt5OZVJh4sMunmwwsRihNJvJu | Soros | Georg |
| 5 | poloosa | $2y$10$Yi4tJ8KkFX6QOTKJR2HOVehSxbrNq6PVreiIf7Htwd.8SLPDCTynC | Kudeříková | Marie |
| 9 | traktor | $2y$10$Jux8JT5NWoi7O5MNxb8dNe9PXo.qr7.FIgjUPjyr4jz9rmD94mxUC | Náhlovský | Josef |
| 11 | vizidlo | $2y$10$D6vJtFZWv3MtrQhiVESGR.WVSrtrN6EXMl49jk/oMadZ1mVO0RulG | Pokorný | Jan |
+-----+---------------+--------------------------------------------------------------+---------------+-----------+
7 rows in set (0.022 sec)
Nyní se přihlásíme do konzolového klienta mariadb, viz výše. Vše by mělo fungovat tak, jak je to popsané. Máme připravenu databázi a tabulku „uzivatele“ k ukázce SQL injekce (SQL injection).
V následující ukázce je již přihlašovací jméno, účet přednastavené. Je to ukázkový příklad neošetřeného PHP kódu proti SQL injektáži.
<?php
echo "<!DOCTYPE html>\n";
echo "<html lang=\"cs\">\n";
echo "<head>\n";
echo "<meta charset=\"utf-8\" />\n";
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n";
echo "<meta name=\"robots\" content=\"all\" />\n";
echo "<meta name=\"keywords\" content=\"Parametrizované,SQL,dotazy,PHP,mysqli\" />\n";
echo "<meta name=\"description\" content=\"Parametrizované SQL dotazy v PHP rozhraní mysqli\" />\n";
echo "<meta name=\"autor\" content=\"Kocour\" />\n";
echo "<title>Parametrizované SQL dotazy v PHP rozhraní mysqli</title>\n";
echo "</head>\n";
echo "<body>\n";
echo "<h1>Parametrizované SQL dotazy v PHP rozhraní mysqli</h1>\n";
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$submit="poslat";
// ============== přihlášení uživatele (admina) =============
if (!empty($_POST[$submit]) and !empty($_POST['ucet']) and !empty($_POST['heslo'])) {
// tady načteme heslo z databáze a dáme do proměnné $hash_heslo
$ucet = $_POST['ucet'];
//$ucet = $mysqli->real_escape_string($ucet); // starší obrana proti SQL injection
$dotaz = "select * from uzivatele where ucet=\"".$ucet."\"";
//echo $dotaz."<br />";
$vysledek = $mysqli->query($dotaz);
$poczaznamu = $vysledek->num_rows;
// ********** SQL injection na Zadej účet: " OR "1"="1 ************ !!!!!!!!!!!!!!!!!!!!!!
// vytvoří SQL dotaz: select * from uzivatele where ucet="" OR "1"="1" !!! To vrátí všechny záznamy
if ($poczaznamu > 0) {
$zaznam = $vysledek->fetch_array(MYSQLI_ASSOC);
$hash_heslo = $zaznam['heslo'];
$heslo2 = $_POST['heslo'];
// porovnání hesel password_hash()-> password_verify()
if (password_verify($heslo2, $hash_heslo)) {
echo 'Správné heslo!';
} else {
echo 'Špatné jméno nebo heslo.<br />';
echo "<pre>";
echo "**********************************************************<br />";
echo " ----- H A C K N U T O !!! ------- <br />";
echo " nezapnutá obrana proti SQL injection !!! <br />";
echo "**********************************************************<br />";
echo "SQL dotaz: ".$dotaz."<br />";
echo "+-----+---------------+--------------------------------------------------------------+---------------+-----------+<br>";
echo "| uid | ucet | heslo | prijm | jmeno |<br>";
echo "+-----+---------------+--------------------------------------------------------------+---------------+-----------+<br>";
echo "| 1 | admin | $2y$10$Zus3PC71xQit/2Bbu/esRO9nUGHHiUCyUASnPkO6mX3Ml0IngGtEm | Světlá | Karolína |<br>";
echo "</pre>";
}
} else {
echo 'Špatné jméno nebo heslo.<br />';
}
}
// ==========================================================
// ============================= přihlašovací formulář ================================================
// účet:admin
// heslo:tajne-heslo-milanku
echo "<form method=\"post\">\n";
echo "<fieldset><br />\n";
echo "<legend>Zadání autorizačních údajů</legend>\n";
echo "Zadej přihlašovací účet<br />";
// SQL injection na Zadej přihlašovací účet: " OR "1"="1
echo "<input type=\"text\" name=\"ucet\" value=\"" OR "1"="1\" size=\"\" /><br /><br />\n";
echo "Zadej heslo<br />";
echo "<input type=\"password\" name=\"heslo\" value=\"123456\" size=\"\" /><br />\n";
echo "<br /><input type=\"submit\" name=\"".$submit."\" value=\"odeslat\" />\n";
echo "</fieldset>\n";
echo "</form>\n";
// ======================================================================================================
$mysqli->close(); // není nutné
echo "</body>\n";
echo "</html>\n";
?>
Zobrazí se nám přihlašovací formulář s předvyplněným hackerským jménem. Přitom heslo může být libovolné a přesto je obejit přihlašovací formulář. Přesto, že jsme měli heslo správně uloženo jako password_hash(), přesto že jsme použili správně password_verify(), hacker se bez ošetřeného kódu proti SQL injektáži k přihlašovacím údajům dostal.
Takovýto neošetřený dotaz z formuláře je skvělé místo pro SQL injekci (injection). Místo očekávaného SQL dotazu: „select * from uzivatele where ucet="admin";“ byl dotaz pozměněn na:
select * from uzivatele where ucet="" OR "1"="1";
To hackerovi umožní obejít autorizační logiku a dodá všechny záznamy z tabulky uživatelé. Pokud ještě byly hesla uložena pomocí zastaralých MD5, SHA1 hashovacích funkcí, tak zná i jejich otevřený tvar a získá plnou kontrolu nad webovou aplikací.
Nyní už známe jak to nedělat. A níže je popsáno jak to dělat s parametrizovanými dotazy.
Ve zdrojovém kódu by stačilo odremovat (zrušit // ) na řádku 24 zapnout obranu proti SQL injekci (injection):
$ucet = $mysqli->real_escape_string($ucet);
Pak by byla aplikace zabezpečená starším a již nedoporučovaným způsobem.
Ale existuje modernější a doporučovaný způsob obrany (nebo ochrany) proti SQL injekci (injection) - parametrizované dotazy v PHP. V parametrizovaných dotazech nevkládáme proměnné do dotazu ale dáváme zástupné znaky, obvykle otazníky. Proměnné předáme později najednou v poli. Bohužel oficiální dokumentace k parametrizovaným dotazům v PHP s rozhraním MySQLi je značně chaotickým mixem zastaralých řešení.
Používáme objektové:
U bodu 2. je důležité a málo zdokumentované $stmt->bind_param($typy, ...$parametry), kdy jsou třeba už jen dvě proměnné, první typu string ($typy="ssssi"), druhá typu pole ($parametry=array($ucet,$heslo,$prijmeni,$jmeno,$uid)).
Už žádné zastaralé $stmt->bind_param('sssd', $code, $language, $official, $percent);bind_result(), call_user_func_array()... ale
$typy = "ssss";
$parametry = array($ucet,$heslo,$prijmeni,$jmeno);
...
$stmt->bind_param($typy, ...$parametry);
Jako typy parametrů (zde proměnná $typy) se používá:
Escapování proměnných u parametrizovaných dotazů již není potřeba ($mysqli->real_escape_string). To ale neznamená, že bychom je neměli vůbec ošetřovat. Minimálně u proměnných omezit délku, oříznutí počátečních a koncových mezer, strip_tags(), zbavit se znaků ", ', <, >.
Příklad výběrového parametrizovaného dotazu v PHP a rozhraní MySQLi (objektová verze). Zde je důležité hlavně pole $parametry = array($ucet,$heslo,$prijmeni,$jmeno); $stmt->bind_param($typy, ...$parametry); To umožňuje snadno používat více parametrů v poli, nikoliv další proměnné, viz INSERT - přídání záznamu parametrizovaně.
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$ucet = $_POST['ucet'];
$delka=15; $ucet = mb_substr(trim($_POST['ucet']),0,$delka,"utf-8"); $ucet=strip_tags($ucet);$ucet = str_replace(array("'",">","<",'"'), array("","","",""), $ucet);
// =================== parametrizovaný dotaz ===============
$dotaz = "select * from uzivatele where ucet=?";
$typy = "s";
$parametry = array($ucet);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
$vysledek = $stmt->get_result();
$stmt->close();
$poczaznamu = $vysledek->num_rows;
while($zaznam = $vysledek->fetch_assoc()) {
echo $zaznam['jmeno']." ".$zaznam['prijm']."<br />";
}
}
$mysqli->close();
parametrizovaný dotaz v MYSQLi
Pokud používáme výhradně parametrizované SQL dotazy a chceme vložit dotaz bez proměnných, stačí vynechat ->bind_param().
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
// =================== parametrizovaný dotaz ===============
$dotaz = "select jmeno,prijm from uzivatele";
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->execute();
$vysledek = $stmt->get_result();
$stmt->close();
$poczaznamu = $vysledek->num_rows;
while($zaznam = $vysledek->fetch_assoc()) {
echo $zaznam['jmeno']." ".$zaznam['prijm']."<br />";
}
}
$mysqli->close();
Příklad pro parametrizovaný dotaz pro vkládání nových záznamů v PHP a s MySQLi jako obrana proti SQL injection (injekci).
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$ucet = mb_substr(trim($_POST['ucet']),0,15,"utf-8");
$heslo = mb_substr(trim($_POST['heslo']),0,50,"utf-8"); $heslo = password_hash($heslo, PASSWORD_DEFAULT);
$prijmeni = mb_substr(trim($_POST['prijmeni']),0,25,"utf-8");
$jmeno = mb_substr(trim($_POST['jmeno']),0,25,"utf-8"); $jmeno = str_replace(array("'",">","<",'"'), array("","","",""), $jmeno);
$dotaz = "insert into uzivatele(ucet,heslo,prijm,jmeno) values(?,?,?,?)";
$typy = "ssss";
$parametry = array($ucet,$heslo,$prijmeni,$jmeno);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
echo $stmt->affected_rows;
$stmt->close();
}
$mysqli->close();
Bezpečná úprava záznamů (editace, update) odolná proti SQL injection (injekci), tedy výhradně s parametrizovanými dotazy v PHP a MySQLi rozhraní.
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$uid = intval($_POST['uid']);
$delka=15; $ucet = mb_substr(trim($_POST['ucet']),0,$delka,"utf-8"); $ucet=strip_tags($ucet);
$delka=50; $heslo = mb_substr(trim($_POST['heslo']),0,$delka,"utf-8"); $heslo = password_hash($heslo, PASSWORD_DEFAULT);
$delka=25; $prijmeni = mb_substr(trim($_POST['prijmeni']),0,$delka,"utf-8"); $prijmeni=strip_tags($prijmeni);
$delka=25; $jmeno = mb_substr(trim($_POST['jmeno']),0,$delka,"utf-8"); $jmeno=strip_tags($jmeno); $jmeno = str_replace(array("'",">","<",'"'), array("","","",""), $jmeno);
$dotaz = "UPDATE uzivatele SET ucet = ?, heslo = ?, prijm = ?, jmeno = ? WHERE uid = ?";
$typy = "ssssi";
$parametry = array($ucet,$heslo,$prijmeni,$jmeno,$uid);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
echo $stmt->affected_rows;
$stmt->close();
}
$mysqli->close();
Mazání záznamů v PHP a rozhraní MySQLi pomocí parametrizovaného dotazu, tedy bezpečné a odolné proti SQL injekci.
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$uid = intval($_POST['uid']);
$dotaz = "DELETE FROM uzivatele WHERE uid = ?";
$typy = "i";
$parametry = array($uid);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
echo $stmt->affected_rows;
$stmt->close();
}
$mysqli->close();
Zjištění ID primárního klíče v parametrizovaném dotazu v PHP a MySQLi.
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$dotaz = "insert into uzivatele(ucet,heslo,prijm,jmeno) values(?,?,?,?)";
$typy = "ssss";
$parametry = array($ucet,$heslo,$prijmeni,$jmeno);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
echo $mysqli->insert_id;
$stmt->close();
}
$mysqli->close();
Použití „LIKE“ v parametrizovaném dotazu je snadné, občas se dělají chyby nebo někdo dokonce říká, že to nejde.
Toto je špatně!
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE Name LIKE %?%");
Celý trik je v tom, že v případě použití LIKE v parametrizovaném dotazu znak % zadáváme jako součást proměnné! ($prijm = "Ma%";)
Ukázka, návod na použití LIKE v parametrizovaném dotazu s objektovým rozhraním MySQLi v PHP nad databází MariaDB (dříve MySQL).
$mysqli = new mysqli("localhost", "testik", "testik", "testik");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
// =================== parametrizovaný dotaz ===============
$prijm = "Ma%";
$typy = "s";
$parametry = array($prijm);
$dotaz = "select * from uzivatele where prijm like ?";
//echo $dotaz."<br />";
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
$vysledek = $stmt->get_result();
$stmt->close();
$poczaznamu = $vysledek->num_rows;
while($zaznam = $vysledek->fetch_assoc()) {
echo $zaznam['jmeno']." ".$zaznam['prijm']."<br />";
}
}
$mysqli->close();
Od PHP 8.1.0 je možné volitelně přidat do $stmt->execute(array(1,2,3)) parametr pole, podobně jako v PDO rozhraní. Tím je možno vynechat $stmt->bind_param($typy, ...$parametry) a používat jen prepare() a execute().
<?php
$mysqli = new mysqli("localhost", "redakcni_system", "moje tajne heslo", "redakcni_system");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$jmeno = "Jan";
$dotaz = "select * from uzivatele where jmeno = ? limit ?";
$parametry = array($jmeno, 2);
$stmt = $mysqli->prepare($dotaz);
$stmt->execute($parametry);
$vysledek = $stmt->get_result();
$stmt->close();
$poczaznamu = $vysledek->num_rows;
while ($zaznam = $vysledek->fetch_assoc()) {
echo $zaznam['Prijmeni']." ".$zaznam['jmeno']."<br />";
}
$mysqli->close();
?>
Odkazy
PHP MySQLi Prepared Statements Tutorial to Prevent SQL Injection
Komentáře
Kdokoliv může přidávat komentáře ke článkům bez registrace. Zadá si libovolnou přezdívku a napíše komentář.
SSL pro weby od 11/2015 zdarma
MS WINDOWS 10, 11 - sběr informací o uživateli
DEBIAN 12 (bookworm) - OS zdarma debian vyšel 10.6.2023
debian - stáhnout nejnovější DEBIAN pro PC
debian edu - debian pro školy a školní prostředí, stažení DEBedu (torrent)
Zranitelnost „ROM-0“ routerů
Předali data tajným službám
Americké bezpečnostní agentuře (NSA) předali data Microsoft, Yahoo, Google, Facebook...
Itálie preferuje open source
Italský parlament schválil zákon, který nařizuje státním institucím pořizovat otevřený software před komerčním. To znamená LINUX místo MS-WINDOWS, LIBRE OFFICE místo MS OFFICE atd.