Prevence SQL injection
- SQL injection útoky jsou velmi časté, což je způsobeno dvěma faktory
- Značný výskyt SQL injection zranitelností
- Atraktivita cíle (databáze - obsahuje zajímavá a kritická aplikační data)
- Chyby vznikají, když vývojáři vytvářejí dynamické databázové dotazy konstruované spojováním řetězců, které zahrnují uživatelské vstupy
- Přestaňte psát dynamické dotazy se spojováním řetězců
- Zabraňte tomu, aby uživatelský vstup obsahující škodlivé SQL, ovlivnil logiku prováděného dotazu
Základní obrana
- Použijte prepared statements
- Správně sestavte uložené procedury
- Validujte vstup na základě povolených seznamů
- Escapujte veškerý uživatelský vstup
Další možná obrana
- Implementujte pravidlo nejnižšího oprávnění
- Validujte vstup na základě seznamu povolených vstupů jako sekundární obranu
Příklad nebezpečného použití
- Útočníkovi by umožnil vložit do dotazu kód, který by databáze provedla
- Neověřený parametr
customerName
, který je jednoduše připojen k dotazu, umožňuje injektovat libovolný SQL kód - Tento přístup k databázím je velmi častý
String query = "SELECT account_balance FROM user_data WHERE user_name = "
+ request.getParameter("customerName");
try {
Statement statement = connection.createStatement( ... );
ResultSet results = statement.executeQuery( query );
}
...
Základní obrana
Použijte prepared statements (query parametrizace)
- Použití prepared statements je způsob, jakým by se všichni vývojáři měli naučit psát databázové dotazy
- Jsou jednoduché a srozumitelnější než dynamické dotazy
- Nutí vývojáře definovat veškerý SQL kód a teprve pak předávat jednotlivé parametry
- To umožňuje rozlišovat mezi SQL kódem a daty bez ohledu na vstup
- Prepared statements zajišťují, že útočník nemůže změnit záměr SQL dotazu
- Pokud by útočník v níže uvedeném (bezpečném) příkladu zadal
userId = tom' or '1'='1
, parametrizovaný dotaz by nebyl zranitelný- Útočník by hledal uživatelské jméno, které by odpovídalo zadanému řetězci
Doporučení pro jednotlivé jazyky
-
Java EE - použijte
PreparedStatement()
-
.NET - použijte parametrizované query jako je
SqlCommand()
neboOleDbCommand()
-
PHP - použijte PDO s parametrizovanými dotazy (
bindParam()
) -
Hibernate - použijte
createQuery()
-
SQLite - použijte
sqlite3_prepare()
-
Ve výjimečných případech mohou mít prepared statements negativní vliv na výkon
-
V takové situaci je lepší
- validovat veškeré vstupy
- escapovat uživatelské vstupy
Příklad bezpečného Java prepared statementu
- Použití
PreparedStatement
parametrizované query
// This should REALLY be validated too
String custname = request.getParameter("customerName");
// Perform input validation to detect attacks
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
Příklad bezpečného .NET prepared statementu
- Předání parametrů dotazu pomocí
Parameters.Add()
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
OleDbCommand command = new OleDbCommand(query, connection);
command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReader reader = command.ExecuteReader();
// …
} catch (OleDbException se) {
// error handling
}
- Prakticky všechny jazyky podporují parametrizované query
- Příklady v jiných jazycích (Ruby, PHP, Perl) najdete v dokumentu o query parametrizaci
Hibernate Query Language (HQL) prepared statements
// First is an unsafe HQL Statement
Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'");
// Here is a safe version of the same query using named parameters
Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);
- Díky prepared statements je aplikace relativně nezávislá na databázi, protože veškerý SQL kód zůstává v aplikaci
Uložené procedury
- Nejsou vždy bezpečné
- Při bezpečné implementaci mají stejný účinek jako parametrizované dotazy
- Uložená procedura neobsahuje nebezpečné dynamické generování SQL
- Vyžadují, aby vývojáři pouze sestavili SQL s parametry, které jsou automaticky parametrizovány
- Pokud vývojář neudělá něco, co se vymyká
- Rozdíl mezi prepared statements je v tom, že SQL procedury jsou definovány a uloženy v databázi a poté volány aplikací
- Obě techniky mají stejnou účinnost při prevenci SQL injection
- Organizace by si měla vybrat přístup, který pro ni má největší smysl
- Validujte vstup a escapujte jej v případě, že uložená procedura obsahuje dynamické dotazy
- Hledejte funkce jako
sp_execute
,execute
neboexec
v rámci uložených procedur
- V případech, kdy uložená procedura pro
execute
funkci vyžaduje příliš vysokou roli (např.db_owner
), může uložená procedura zvýšit riziko- V případě narušení serveru má útočník plná nebo zvýšená práva k databázi
Java - bezpečné použití uložené procedury
CallableStatement
je implementace uložené procedury v jazyce Java
// This should REALLY be validated
String custname = request.getParameter("customerName");
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
// … result set handling
} catch (SQLException se) {
// … logging and error handling
}
VB .NET - bezpečné použití uložené procedury
SqlCommand
asp_getAccountBalance
(která by musela být předem definována) jsou implementace uložené procedury v prostředí .NET
// This should REALLY be validated
String custname = request.getParameter("customerName");
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
// … result set handling
} catch (SQLException se) {
// … logging and error handling
}
Validujte vstup na základě seznamu povolených vstupů
- Některé části SQL dotazu nelze nahrazovat proměnnými (např. názvy tabulek a sloupců, řazení)
- V takových situacích je nejvhodnější vstup validovat nebo dotaz předělat
- V případě názvů tabulek a sloupců je ideální, když hodnoty pocházejí z kódu, a ne z uživatelských parametrů
- Pokud se takové parametry používají, měly by být mapovány na očekávané názvy tabulek a sloupců
- Tím se zajistí, že se v dotazu neobjeví uživatelský vstup
- V každém případě se však jedná o špatný návrh a mělo by se zvážit přepsání dané funkcionality
- Zde je příklad validace názvu tabulky
String tableName;
switch(PARAM):
case "Value1": tableName = "fooTable";
break;
case "Value2": tableName = "barTable";
break;
...
default : throw new InputValidationException("unexpected value provided"
+ " for table name");
tableName
lze přímo připojit k SQL dotazu, protože je známo, že je jednou z očekávaných hodnot- Obecné funkce pro validaci mohou vést ke ztrátě dat, protože názvy tabulek jsou použity v dotazech, kde nejsou očekávány
- Pro pořadí řazení je nejlepší, kdyby se vstup zadaný uživatelem převedl na
boolean
a ten se následně použil k výběru bezpečné hodnoty, která se připojí k dotazu
public String someMethod(boolean sortOrder) {
String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");`
...
- Kdykoli je možné vstup převést na jiný než
String
typ (datum, číslo, boolean, atp.), je zajištěno, že je to bezpečné - Validace vstupů se doporučuje jako sekundární obrana ve všech případech (i při použití
bind
proměnných) - Další techniky, jak implementovat validaci vstupů najdete v cheat sheetu
Escapujte veškerý uživatelský vstup
- Tato technika by měla být použita pouze jako poslední možnost
- Vždy se pokuste nejdříve implementovat výše uvedené techniky
- Tato metoda nemůže zaručit zabránění všem případům SQL injection
- Technika slouží k escapování uživatelského vstupu před jeho vložením do dotazu
- Implementace je specifická pro danou databázi
- Obvykle se doporučuje pouze jako dodatečná ve sterším kódu, pokud není možné implementovat validaci vstupu (např. proto, že je to velmi nákladné)
- Aplikace budované od nuly nebo vyžadující nízkou toleranci rizika by měly používat parametrizované dotazy, uložené procedury nebo ORM, které sestavuje dotazy za vývojáře
- Každý DBMS podporuje různá schémata escapování znaků specifických pro určité typy dotazů
- Pokud se tyto vstupy escapují pomocí správného schématu, DBMS si je nesplete s SQL kódem, čímž je možné se vyhnout SQL injection
- OWASP Enterprise Security API (ESAPI) je open source knihovna pro kontrolu zabezpečení webových aplikací
- Usnadňuje programátorům psaní lower-risk aplikací
- Knihovny ESAPI jsou navrženy tak, aby usnadňovaly dodatečnou instalaci zabezpečení do stávajících aplikací
- ESAPI podporuje Oracle DB a MySQL a připravuje se podpora pro SQL Server a PostgreSQL
Příklady
- PHP PDO
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
// Do something with $row
}
- PHP MySQLi
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// Do something with $row
}
Další možná obrana
Použijte pravidlo nejnižšího oprávnění
- Minimalizujte oprávnění přiřazená každému databázovému účtu
- Nepřidělujte aplikčním účtům přístupová práva typu DBA nebo admin
- Určete, jaká přístupová práva vaše účty vyžadují, místo abyste se snažili zjistit, jaká práva je potřeba odebrat
- Zajistěte, aby účty, které mají přístup pouze pro čtení, měly přístup pro čtení tabulek, ke kterým potřebují přístup
- Zvažte vytvoření
view
, pokud účet potřebuje přístup pouze k části tabulky - Nepřidělujte (nebo jen zřídka) práva k vytváření nebo mazání
- Omezte oprávnění účtů tak, aby mohly spouštět pouze ty uložené procedury, které potřebují
- Neudělujte jim žádná práva přímo k tabulkám v databázi
- Minimalizace oprávnění sníží pravděpodobnost pokusů o neoprávněný přístup
- I v případě, že se útočník nepokouší použít SQL injection jako součást exploitu
- Minimalizujte oprávnění účtu OS, pod kterým DBMS běží
- Nespouštějte DBMS jako
root
nebosystem
- Většina DBMS ve výchozím nastavení běží pod velmi silným systémovým účtem
- Například MySQL ve výchozím nastavení běží jako
system
- Změňte OS účet DBMS na nějaký vhodnější s omezenými právy
Více uživatelů databáze
- Vyvarujte se používání stejného účtu (owner, admin) pro připojení k databázi
- Použijte různé DB uživatele pro různé aplikace
- Určete DB účet každé samostatné aplikaci, která vyžaduje přístup do databáze
- Tím zaručíte dobrou granularitu v řízení přístupu a omezíte oprávnění
- Každý uživatel bude mít přístup pouze k tomu, co potřebuje
- Například přihlašovací stránka vyžadující přístup pro čtení k polí uživatelské jméno a heslo
- Nevyžaduje zápis (nic neukládá)
- Stránka s registrací však právo na zápis vyžaduje
- Takové omezení lze prosadit pouze v případě, že aplikace používají k připojení k databázi různé DB uživatele
Views
- Pomocí SQL views je možné zvýšit granularitu přístupu - omezení přístupu pro čtení na konkrétní pole tabulky nebo spojení tabulek
Validace vstupů na základě povolených vstupů
- Slouží k odhalení neoprávněného vstupu před předáním do SQL dotazu
- Primární obrana v případě, že nelze použít nic jiného
- Vhodná také jako sekundární obrana
- Zvalidovaná data nemusí být nutně bezpečná pro vkládání prostřednictvím sestavování SQL řetězců
- Více informací najdete v cheat sheetu o validaci vstupu