Přeskočit na hlavní obsah

Prevence SQL injection

  • SQL injection útoky jsou velmi časté, což je způsobeno dvěma faktory
    1. Značný výskyt SQL injection zranitelností
    2. 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
  1. Přestaňte psát dynamické dotazy se spojováním řetězců
  2. Zabraňte tomu, aby uživatelský vstup obsahující škodlivé SQL, ovlivnil logiku prováděného dotazu

Základní obrana

  1. Použijte prepared statements
  2. Správně sestavte uložené procedury
  3. Validujte vstup na základě povolených seznamů
  4. Escapujte veškerý uživatelský vstup

Další možná obrana

  1. Implementujte pravidlo nejnižšího oprávnění
  2. 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() nebo OleDbCommand()

  • 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
  1. Validujte vstup a escapujte jej v případě, že uložená procedura obsahuje dynamické dotazy
  2. Hledejte funkce jako sp_execute, execute nebo exec 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 a sp_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í

  1. Minimalizujte oprávnění přiřazená každému databázovému účtu
  2. Nepřidělujte aplikčním účtům přístupová práva typu DBA nebo admin
  3. 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
  4. 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
  5. Zvažte vytvoření view, pokud účet potřebuje přístup pouze k části tabulky
  6. Nepřidělujte (nebo jen zřídka) práva k vytváření nebo mazání
  7. Omezte oprávnění účtů tak, aby mohly spouštět pouze ty uložené procedury, které potřebují
    1. 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
  1. Minimalizujte oprávnění účtu OS, pod kterým DBMS běží
  2. Nespouštějte DBMS jako root nebo system
    • 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
    1. Změňte OS účet DBMS na nějaký vhodnější s omezenými právy

Více uživatelů databáze

  1. Vyvarujte se používání stejného účtu (owner, admin) pro připojení k databázi
  2. Použijte různé DB uživatele pro různé aplikace
  3. 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

Kam dál

Zranitelnosti

Cheat sheety

Checklisty