From 9ac3313f192d42c8cbacb2f030773f5dc7de2948 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 18 Dec 2024 13:31:41 +0100 Subject: [PATCH] wip --- database/cs/security.texy | 127 +++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/database/cs/security.texy b/database/cs/security.texy index 07eede6987..147ad89eb7 100644 --- a/database/cs/security.texy +++ b/database/cs/security.texy @@ -1,29 +1,38 @@ Bezpečnostní rizika ******************* -.[perex] -Databáze často obsahuje citlivá data a umožňuje provádět nebezpečné operace. Nette Database nabízí řadu bezpečnostních prvků. Klíčové je ale pochopení rozdílu mezi bezpečným a nebezpečným API. +
+Databáze často obsahuje citlivá data a umožňuje provádět nebezpečné operace. Pro bezpečnou práci s Nette Database je klíčové: -SQL Injection -============= +- Porozumět rozdílu mezi bezpečným a nebezpečným API +- Používat parametrizované dotazy +- Správně validovat vstupní data -SQL injection je nejzávažnější bezpečnostní riziko při práci s databází. Vzniká, když se neošetřený vstup od uživatele stane součástí SQL dotazu. Útočník může vložit vlastní SQL příkazy a tím získat nebo modifikovat data v databázi. +
+ + +Co je SQL Injection? +==================== + +SQL injection je nejzávažnější bezpečnostní riziko při práci s databází. Vzniká, když se neošetřený vstup od uživatele stane součástí SQL dotazu. Útočník může vložit vlastní SQL příkazy a tím: +- Získat neoprávněný přístup k datům +- Modifikovat nebo smazat data v databázi +- Obejít autentizaci ```php // ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection $database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); // Útočník může zadat například hodnotu: ' OR '1'='1 -// Výsledný dotaz pak bude: -// SELECT * FROM users WHERE name = '' OR '1'='1' -// Což vrátí všechny uživatele! +// Výsledný dotaz pak bude: SELECT * FROM users WHERE name = '' OR '1'='1' +// Což vrátí všechny uživatele ``` Totéž se týká i Database Explorer: ```php -// ❌ NEBEZPEČNÝ KÓD +// ❌ NEBEZPEČNÝ KÓD - zranitelný vůči SQL injection $table->where('name = ' . $_GET['name']); $table->where("name = '$_GET[name]'"); ``` @@ -34,83 +43,87 @@ Bezpečné parametrizované dotazy Bezpečným způsobem vkládání hodnot do SQL dotazů jsou parametrizované dotazy. Nette Database nabízí několik způsobů jejich použití. - -Zástupné otazníky ------------------ - -Nejjednodušší způsob je použití zástupných otazníků: +Nejjednodušší způsob je použití **zástupných otazníků**: ```php -// ✅ Bezpečné parametrizované dotazy -$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']); +// ✅ Bezpečný parametrizovaný dotaz +$database->query('SELECT * FROM users WHERE name = ?', $name); // ✅ Bezpečná podmínka v Exploreru -$table->where('name = ?', $_GET['name']); +$table->where('name = ?', $name); ``` -Totéž platí pro všechny další metody v Database Explorer, které umožňují vkládat výrazy se zástupnými otazníky a parametry. - -.[warning] -Hodnoty musí být skalárního typu (string, int, float, bool) nebo null. Pokud by například `$_GET['name']` bylo pole, Nette Database by vložil do SQL všechny jeho prvky, což může být nežádoucí. - - -Pole hodnot ------------ +Tohle platí pro všechny další metody v [Database Explorer|explorer], které umožňují vkládat výrazy se zástupnými otazníky a parametry. -Pro příkazy INSERT, UPDATE nebo klauzule WHERE můžeme použít pole hodnot: +Pro příkazy INSERT, UPDATE nebo klauzuli WHERE můžeme bezpečně předat hodnoty v poli: ```php // ✅ Bezpečný INSERT $database->query('INSERT INTO users', [ - 'name' => $_GET['name'], - 'email' => $_GET['email'], + 'name' => $name, + 'email' => $email, ]); -// ✅ Bezpečný UPDATE -$database->query('UPDATE users SET', [ - 'name' => $_GET['name'], - 'email' => $_GET['email'], -], 'WHERE id = ?', $_GET['id']); +// ✅ Bezpečný INSERT v Exploreru +$table->insert([ + 'name' => $name, + 'email' => $email, +]); ``` -Nette Database automaticky escapuje všechny hodnoty předané přes parametrizované dotazy. Musíme však zajistit správný datový typ parametrů. +.[warning] +Musíme však zajistit správný datový typ parametrů. + + +Validace vstupních dat +====================== + +**Databáze předpokládá**, že všechna vstupní data už prošla validací. Nikdy do ní nesmí vstoupit nevalidovaná data - validační mezivrstva je povinná součást bezpečné práce s databází. + +Validace musí zajistit: + +1. Správný datový typ - parametry musí mít přesně ten typ, který se očekává. Například pokud by `$name` v příkladu výše bylo pole místo řetězce, Nette Database by se pokusilo vložit všechny jeho prvky do SQL dotazu, což by mohlo být nežádoucí. +2. Validní obsah - například u řetězců je třeba oveřit validní UTF-8 kódování nebo že maximální délka odpovídá definici sloupce v databázi, i čísel, že jejich hodnota je v povoleném rozsahu pro daný sloupec atd. + +Proto nikdy nepoužívejte nevalidovaná data z `$_GET`, `$_POST` nebo `$_COOKIE` přímo v databázových dotazech. Místo toho: + +- Používejte [Nette Formuláře|forms:], které automaticky zajistí správnou validaci všech vstupů +- Nebo implementujte vlastní validační vrstvu pomocí standardních PHP nástrojů jako `filter_var()` Klíče polí nejsou bezpečné API ============================== -Zatímco hodnoty v polích jsou bezpečné, o klíčích to neplatí: +Zatímco hodnoty v polích jsou bezpečné, o klíčích to neplatí! ```php -// ❌ NEBEZPEČNÝ KÓD - klíče mohou obsahovat SQL injection -$database->query('INSERT INTO users', $_GET); -$database->query('SELECT * FROM users WHERE', $_GET); -$table->where($_GET); +// ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli +$database->query('INSERT INTO users', $_POST); ``` -U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců. +U příkazů INSERT a UPDATE je to zásadní bezpečnostní chyba - útočník může do databáze vložit nebo změnit jakýkoliv sloupec. Mohl by si například nastavit `is_admin = 1` nebo vložit libovolná data do citlivých sloupců (tzv Mass Assignment Vulnerability). -Ve WHERE podmínkách je to ještě nebezpečnější, protože umožňuje SQL enumeration - techniku postupného zjišťování informací o databázi. Útočník může třeba zkoumat plat zaměstnanců tím, že do `$_GET` podstrčí: +Ve WHERE podmínkách je to ještě nebezpečnější, protože mohou obsahovat oprátory: ```php -$_GET = ['salary >', 100000]; // začne zjišťovat platové rozsahy +// ❌ NEBEZPEČNÝ KÓD - nejsou ošetřené klíče v poli +$_POST['salary >'] = 100000; +$database->query('SELECT * FROM users WHERE', $_POST); +// vykoná dotaz WHERE (`salary` > 100000) ``` -Ale hlavní problém je, že WHERE podmínky podporují v klíčích SQL výrazy: +Útočník může tento přístup využít k systematickému zjišťování platů zaměstnanců. Začne například dotazem na platy nad 100.000, pak pod 50.000 a postupným zužováním rozsahu může odhalit přibližné platy všech zaměstnanců. Tento typ útoku se nazývá SQL enumeration. -```php -// Legitimní použití operátorů v klíčích -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], -]); +Metoda `where()` podporuje v klíčích SQL výrazy včetně operátorů a funkcí. To dává útočníkovi možnost provést komplexní SQL injection: -// ❌ NEBEZPEČNÉ: útočník může vložit vlastní SQL -$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1]; -$table->where($_GET); // umožní útočníkovi získat platy adminů +```php +// ❌ NEBEZPEČNÝ KÓD - útočník může vložit vlastní SQL +$_POST['0) UNION SELECT name, salary FROM users WHERE (?'] = 1; +$table->where($_POST); +// vykoná dotaz WHERE (0) UNION SELECT name, salary FROM users WHERE (1) ``` -Toto je opět **SQL injection**. +Tento útok ukončí původní podmínku pomocí `0)`, připojí vlastní `SELECT` pomocí `UNION` aby získal citlivá data z tabulky `users` a uzavře syntakticky správný dotaz pomocí `WHERE (1)`. Whitelist sloupců @@ -121,7 +134,7 @@ Pokud chcete uživateli umožnit volbu sloupců, vždy použijte whitelist: ```php // ✅ Bezpečné zpracování - pouze povolené sloupce $allowedColumns = ['name', 'email', 'active']; -$values = array_intersect_key($_GET, array_flip($allowedColumns)); +$values = array_intersect_key($_POST, array_flip($allowedColumns)); $database->query('INSERT INTO users', $values); ``` @@ -130,16 +143,18 @@ $database->query('INSERT INTO users', $values); Dynamické identifikátory ======================== -Pro dynamické názvy tabulek a sloupců použijte zástupný symbol `?name`: +Pro dynamické názvy tabulek a sloupců použijte zástupný symbol `?name`. Ten zajistí správné escapování identifikátorů podle syntaxe dané databáze (např. pomocí zpětných uvozovek v MySQL): ```php // ✅ Bezpečné použití důvěryhodných identifikátorů $table = 'users'; $column = 'name'; $database->query('SELECT ?name FROM ?name', $column, $table); +// Výsledek v MySQL: SELECT `name` FROM `users` // ❌ NEBEZPEČNÉ - nikdy nepoužívejte vstup od uživatele $database->query('SELECT ?name FROM users', $_GET['column']); ``` -Symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět whitelist. +Důležité: symbol `?name` používejte pouze pro důvěryhodné hodnoty definované v kódu aplikace. Pro hodnoty od uživatele použijte opět whitelist. +