Riscuri de securitate

Baza de date conține adesea date sensibile și permite efectuarea de operațiuni periculoase. Pentru a lucra în siguranță cu Nette Database, este esențial să:

  • Înțelegeți diferența dintre API-ul sigur și cel nesigur
  • Utilizați interogări parametrizate
  • Validați corect datele de intrare

Ce este SQL Injection?

SQL injection este cel mai grav risc de securitate atunci când lucrați cu o bază de date. Apare atunci când intrarea nesanitizată de la utilizator devine parte a unei interogări SQL. Atacatorul poate introduce propriile comenzi SQL și astfel:

  • Obține acces neautorizat la date
  • Modifică sau șterge datele din baza de date
  • Ocolește autentificarea
// ❌ COD PERICULOS - vulnerabil la SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Atacatorul poate introduce, de exemplu, valoarea: ' OR '1'='1
// Interogarea rezultată va fi: SELECT * FROM users WHERE name = '' OR '1'='1'
// Ceea ce returnează toți utilizatorii

Același lucru este valabil și pentru Database Explorer:

// ❌ COD PERICULOS - vulnerabil la SQL injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Interogări parametrizate

Apărarea de bază împotriva SQL injection sunt interogările parametrizate. Nette Database oferă mai multe moduri de a le utiliza.

Cel mai simplu mod este utilizarea semnelor de întrebare placeholder:

// ✅ Interogare parametrizată sigură
$database->query('SELECT * FROM users WHERE name = ?', $name);

// ✅ Condiție sigură în Explorer
$table->where('name = ?', $name);

Acest lucru este valabil pentru toate celelalte metode din Database Explorer, care permit inserarea de expresii cu semne de întrebare placeholder și parametri.

Pentru comenzile INSERT, UPDATE sau clauza WHERE, putem transmite valorile într-un array:

// ✅ INSERT sigur
$database->query('INSERT INTO users', [
	'name' => $name,
	'email' => $email,
]);

// ✅ INSERT sigur în Explorer
$table->insert([
	'name' => $name,
	'email' => $email,
]);

Validarea valorilor parametrilor

Interogările parametrizate sunt piatra de temelie a lucrului sigur cu baza de date. Cu toate acestea, valorile pe care le introducem în ele trebuie să treacă prin mai multe niveluri de control:

Controlul tipului

Cel mai important este să se asigure tipul corect de date al parametrilor – aceasta este o condiție necesară pentru utilizarea sigură a Nette Database. Baza de date presupune că toate datele de intrare au tipul de date corect corespunzător coloanei respective.

De exemplu, dacă $name din exemplele anterioare ar fi în mod neașteptat un array în loc de un șir, Nette Database ar încerca să insereze toate elementele sale în interogarea SQL, ceea ce ar duce la o eroare. Prin urmare, nu utilizați niciodată date nevalidate din $_GET, $_POST sau $_COOKIE direct în interogările bazei de date.

Controlul formatului

La al doilea nivel, verificăm formatul datelor – de exemplu, dacă șirurile sunt în codificare UTF-8 și lungimea lor corespunde definiției coloanei, sau dacă valorile numerice se încadrează în intervalul permis pentru tipul de date al coloanei respective.

La acest nivel de validare, ne putem baza parțial și pe baza de date însăși – multe baze de date vor refuza datele nevalide. Cu toate acestea, comportamentul poate varia, unele pot scurta în tăcere șirurile lungi sau pot trunchia numerele în afara intervalului.

Controlul domeniului

Al treilea nivel constă în controale logice specifice aplicației dvs. De exemplu, verificarea faptului că valorile din casetele de selecție corespund opțiunilor oferite, că numerele se încadrează în intervalul așteptat (de exemplu, vârsta 0–150 de ani) sau că dependențele reciproce dintre valori au sens.

Metode de validare recomandate

  • Utilizați Formulare Nette, care asigură automat validarea corectă a tuturor intrărilor
  • Utilizați Presentere și specificați tipurile de date pentru parametrii din metodele action*() și render*()
  • Sau implementați propriul strat de validare folosind instrumente PHP standard precum filter_var()

Lucrul sigur cu coloanele

În secțiunea anterioară, am arătat cum să validăm corect valorile parametrilor. Cu toate acestea, atunci când folosim array-uri în interogările SQL, trebuie să acordăm aceeași atenție și cheilor lor.

// ❌ COD PERICULOS - cheile din array nu sunt tratate
$database->query('INSERT INTO users', $_POST);

Pentru comenzile INSERT și UPDATE, aceasta este o eroare de securitate fundamentală – atacatorul poate introduce sau modifica orice coloană în baza de date. Ar putea, de exemplu, să seteze is_admin = 1 sau să introducă date arbitrare în coloane sensibile (așa-numita Mass Assignment Vulnerability).

În condițiile WHERE, este și mai periculos, deoarece pot conține operatori:

// ❌ COD PERICULOS - cheile din array nu sunt tratate
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// execută interogarea WHERE (`salary` > 100000)

Atacatorul poate folosi această abordare pentru a descoperi sistematic salariile angajaților. De exemplu, începe cu o interogare pentru salarii peste 100.000, apoi sub 50.000 și, prin restrângerea treptată a intervalului, poate descoperi salariile aproximative ale tuturor angajaților. Acest tip de atac se numește SQL enumeration.

Metodele where() și whereOr() sunt și mult mai flexibile și suportă expresii SQL în chei și valori, inclusiv operatori și funcții. Acest lucru îi oferă atacatorului posibilitatea de a efectua SQL injection:

// ❌ COD PERICULOS - atacatorul poate introduce propriul SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// execută interogarea WHERE (0) UNION SELECT name, salary FROM users WHERE (1)

Acest atac încheie condiția originală folosind 0), adaugă propriul SELECT folosind UNION pentru a obține date sensibile din tabelul users și închide interogarea sintactic corectă folosind WHERE (1).

Lista albă a coloanelor

Pentru a lucra în siguranță cu numele coloanelor, avem nevoie de un mecanism care să asigure că utilizatorul poate lucra doar cu coloanele permise și nu poate adăuga propriile coloane. Am putea încerca să detectăm și să blocăm numele de coloane periculoase (lista neagră), dar această abordare nu este fiabilă – atacatorul poate găsi întotdeauna o nouă modalitate de a scrie un nume de coloană periculos pe care nu l-am prevăzut.

Prin urmare, este mult mai sigur să inversăm logica și să definim o listă explicită de coloane permise (lista albă):

// Coloane pe care utilizatorul le poate modifica
$allowedColumns = ['name', 'email', 'active'];

// Eliminăm toate coloanele nepermise din intrare
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));

// ✅ Acum putem folosi în siguranță în interogări, cum ar fi:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);

Identificatori dinamici

Pentru numele dinamice de tabele și coloane, utilizați substituentul ?name. Acesta asigură escaparea corectă a identificatorilor conform sintaxei bazei de date respective (de exemplu, folosind ghilimele inverse în MySQL):

// ✅ Utilizare sigură a identificatorilor de încredere
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Rezultat în MySQL: SELECT `name` FROM `users`

Important: utilizați simbolul ?name numai pentru valori de încredere definite în codul aplicației. Pentru valorile de la utilizator, utilizați din nou lista albă. Altfel, vă expuneți riscurilor de securitate:

// ❌ PERICULOS - nu utilizați niciodată intrarea de la utilizator
$database->query('SELECT ?name FROM users', $_GET['column']);
versiune: 4.0