SQL Injector vulnerabil

De curand am observart o vullerabilitate in scriptul meu…

Ce imi recomandati ?

vullnerabilitate $_GET['id'];

$id1= mysql_real_escape_string($_GET['id']);

cat de buna este aceasta varianta ?

Functia mysql_real_escape_string nu este recomandata. Din php 5.5.0 este considerata deprecated iar in php 7.0.0 a fost eliminata. Poti sa folosesti in schimb mysqli_real_escape_string.

 $connection = mysqli_connect("localhost", "my_user", "my_password", "my_db");
 
 $id1 = mysqli_real_escape_string($connection, $_GET['id']);

Edit: mysqli_real_escape_string o poti folosi pentru orice input. Daca vrei totusi o executie cat mai rapida si validezi doar input-uri numerice posti folosi:

if (is_numeric($_GET['id']) && $_GET['id'] > 0) {
    // id-ul este numeric si este > 0
} else {
    // id-ul nu este valid
}

Sa inteleg ca ar trebui sa renunt de tot la mysql si sa trec la mysqli ?

Mysqli sau PDO. Recomand PDO.

1 Like

cel mai probabil acel id e numeric intreg deci trebuie sa te asiguri ca e cifra inainte de a-l folosi in interogare

$id = intval($_GET[‘id’]);

Depinde ce reprezinta $_GET[‘id’] si cum il folosesti.
De exemplu, daca este un numar, ar fi mai usor sa verifici cu is_numeric

if(is_numeric($_GET['id'])) {
    $id = (int) $_GET['id'];
} else {
    $error[] = ''; //mesaj de eroare, sau ce vrei tu...
}

Pentru litere, cifre si alte caractere ai functia preg_match si folosesti expresii regulate.
Sau, in functie de extensiile php instalate, poti folosi functii ctype sau filter.

1 Like

cel mai sigur mod pentru protejarea impotriva sql injection-ului sunt expresiile regulate (regex), doar ca necesita pattern in parte pentru fiecare input, ceea ce pe mine ma cam enerveaza.

2 Likes

Exista o gramada de bypass-uri, de exemplu folosind comentarii (/* query */), hex, si alte caractere ciudate. Prin urmare, nu prea poate sa existe o functie care sa se potriveasca pentru toate cazurile. De ex, daca tu astepti un integer, e mult mai safe, sa folosti, intval(), spre ex, decat real_escape_string.

1 Like

query binding si scapi de toate problemele
pt chestii simple folosesc redbeanphp, un ORM care ma scapa de orice, inclusiv de generarea de tabele.

2 Likes

Poți să dai și un exemplu de QB implementat cu PHP chior?

Cât despre a doua parte, mergând pe ideea de aici și citând din clasici în viață: :smiley:

1 Like

PDO se pune?
http://php.net/manual/ro/pdostatement.bindparam.php

SQLi
http://php.net/manual/ro/mysqli.prepare.php

Oricu, de cand am descoperit redbeanphp nu am mai scris un CRUD in SQL.

PS: am facut si un hack sa poata fie folosit in composer: GitHub - necenzurat/redbeanphp-composer: How to install RedBeanPHP through composer

1 Like

Ahh, asta e binding :slight_smile:

Eu am tot folosit Placeholders, dar habar n-aveam că se numește query binding.

#TIL

1 Like

Si daca nu e cifra cum am in cazul search ?

Tot ce s-a menționat aici în afară de mysqli_real_escape_string și prepared statements (regex, intval, etc) ține de validarea integrității datelor, nu de securitate. Cum am spus și-ntr-un alt topic, conținutul fiecarui câmp (e irelevant dacă-i trimis prin GET, POST, PUT, etc. sau cu MIME Type / Content-Type setat ca application/x-www-form-urlencoded, multipart/form-data, application/json sau altceva ) trebuie verificat astfel încât valorile să fie valide. Asta nu are nici o treabă cu securitatea!

Acum, tot ce urmează se aplică strict funcției real_escape_string. Dacă folosești prepared statements, următoarele sunt irelevante pentru că, în cazul acestora, securizarea valorilor se va face automat.


De exemplu: poți avea un bug traker cu un formular unde utilizatorii pot raporta vulnerabilități de tip SQL Injection prin intermediul unui formular de tip WYSIWYG. Acel formular acceptă cod care în sine reprezintă un exploit. Soluția nu este să creezi un Lexer/Parser pentru acel câmp care să analizeze conținutul câmpului și să înțeleagă ce anume reprezintă o vulnerabilitate SQL ci, pur și simplu, să aplici corect real_escape_string conținutului acelui câmp.

Totuși, sunt câteva șmecherii de care trebuie să ții cont.

  1. După ce a fost deschisă conexiunea la baza de date, să fie setat charset-ul corespunzător, altfel real_escape_string nu va ști ce set de caractere trebuie folosit.
  2. Folosește ghilimele în jurul valorii ce va fi folosită în query.

De ce ghilimele?

Să zicem că avem următoarele:

$page  = $_GET['page_id'];
$query = 'SELECT * FROM `pages` WHERE `id` = ' . $page;
$result = $mysqli->query($page);

Cum arată o posibilă injectare în cazul ăsta? Pentru un URL de forma
view.php?page_id=0; DELETE FROM users
interogarea devine:

SELECT * FROM `pages` WHERE `id` = 0; DELETE FROM users

Query-ul ar rămâne la fel chiar dacă am folosi

$page = $mysqli->escape_string($_GET['page_id']);

Interogările multiple sunt un vector de atac posibil în unele drivere MySQL. Din fericire, nu în PHP folosind MySQLi::query(). Acest query va genera un mesaj de eroare de forma

'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DELETE FROM users' at line ...'

Totuși, dacă din diverse motive (posibil justificate) este folosită funcția MySQLi::multi_query(), query-ul multiplu va fi executat fără probleme și ne vom trezi cu un tabel de utilizatori gol!

Dacă folosești și ghilimele

$query = 'SELECT * FROM `pages` WHERE `id` = "' . $page . '"';

query-ul devine:

SELECT * FROM `pages` WHERE `id` = "0; DELETE FROM users"

și e OK (dar asta nu e tot; citește în continuare).


Un alt exemplu, ce nu implică query-uri multiple ar fi următorul:

$user = $_POST['user'];
$pass = $_POST['pass'];

$query = 'SELECT * FROM `users` 
    WHERE `user` = "' . $user . '" 
      AND `pass` = "' . $pass . '"
    LIMIT 1';

$result = $mysqli->query($query);

if ($result->num_rows) {
  $currentUser = $result->fetch_assoc();
} else {
  $error = 'Wrong user / pass combination';
}

Să presupunem că

$_POST['user'] = 'admin';
$_POST['pass'] = '" OR "1"="1';

Astfel, query-ul devine

SELECT * FROM `users` 
    WHERE `user` = "admin" 
      AND `pass` = "" OR "1"="1"
    LIMIT 1

…și oricine se poate autentifica cu orice username existent, introducând " OR "1"="1 în câmpul pentru parolă.

Dacă s-ar folosi real_escape_string în cazul de mai sus, parola ar deveni \" OR \"1\"=\"1 și query-ul n-ar mai fi vulnerabil.

SELECT * FROM `users` 
    WHERE `user` = "admin" 
      AND `pass` = "\" OR \"1\"=\"1"
    LIMIT 1

În concluzie:

Dacă nu sunt folosite prepared statements, sunt 3 lucruri de făcut:

  1. Trebuie setat charset-ul corect după deschiderea conexiunii, folosind $mysqli->set_charset('charset-ul folosit de tine'); (atenție la format: pentru UTF-8 trebuie utf8! utf-8 nu este valid și va fi ignorat).
  2. Valorile tuturor câmpurilor folosite într-un query trebuie securizate cu real_escape_string ȘI
  3. Valorile tuturor câmpurilor folosite într-un query trebuie puse între ghilimele

Expresiile regulate NU asigură securitatea parametrilor, ci validitatea formatului lor.

Atenție!

În situații de genul:

$filter = '';

if ($_GET['filter_by_author']) {
  $filter = ' AND `author` = ' . $_GET['filter_by_author'];
}

$query = 'SELECT * FROM `articles` WHERE `status` = "published" ' . $filter;

real_escape_string și ghilimelele se aplică așa:

if ($_GET['filter_by_author']) {
  $filter = ' AND `author` = "' . $mysqli->escape_string($_GET['filter_by_author']) . '"';
}

și NU așa:

$query = 'SELECT * FROM `articles` WHERE `status` = "published" "' . $mysqli->escape_string($filter) . '"';
6 Likes

E logic ca ele nu au fost concepute pentru a securiza parametrii, dar pot fi folosite și în acest sens. Poți sa folosești un pattern ca sa detecteze cuvintele cheie din mysql și caracterele ciudate, astfel încât sa previi un atac.
Când ziceam de regex în acest context, era clar ca nu ma refer la validarea unui e-mail.

Aceeași idee și la intval(). Cu intval () n-o sa poți sa faci niciodată un atac, pe când cu real escape string, s-ar putea, ca exista bypass-uri, după cum am mai spus, mai sus.

1 Like

Dă-mi un exemplu funcțional de bypass!

\xbf\x27 OR 1=1 /*

1 Like

Văd că nu înțelegem același lucru prin exemplu funcțional! :slight_smile:
Am să simplific un pic lucrurile pentru tine. Îți dau codul PHP și structura bazei de date iar tu spune-mi doar ce trebuie introdus în formular pentru a extrage mai mult de un rând.

-- structura bazei de date

 CREATE TABLE IF NOT EXISTS `test` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=5 ;

INSERT INTO `test` (`id`, `name`) VALUES
(1, 'unu'),
(2, 'doi'),
(3, 'trei'),
(4, 'patru');

codul PHP

<form action="" method="post">
    <input type="text" name="id" value="<?php echo htmlentities(@$_REQUEST['id'], ENT_QUOTES, 'UTF-8')?>" />
    <input type="submit" value="GO!" />
    
    <hr />
    <label><input type="radio" name="quotes" value="0" <?php echo @$_REQUEST['quotes'] != 1 ? 'checked' : ''?> /> Single quotes</label><br />
    <label><input type="radio" name="quotes" value="1" <?php echo @$_REQUEST['quotes'] == 1 ? 'checked' : ''?> /> Double quotes</label>
</form>

<hr />

<?php

$DB = new MySQLi('localhost', 'root', 'root', 'test');

if ($DB->connect_error) {
    die($DB->error);
}

$DB->set_charset('utf8');

if (!isset($_REQUEST['id'])) {
    die('Send the `id` field through either GET or POST!');
}

$quotes = @$_REQUEST['quotes'] == 1 ? '"' : "'";

$id  = $DB->escape_string($_REQUEST['id']);
$sql = 'SELECT * FROM `test` WHERE `id` = ' . $quotes . $id . $quotes;
$res = $DB->query($sql);

echo '<pre>' . $sql . '</pre>';

if ($DB->error) {
    echo '<pre>' . $DB->error . '</pre>';
}


if (!$res) {
    die('<h1>Not this time!</h1>');
}

echo 'You got ' . $res->num_rows . ' row(s)';
echo '<pre>';

while ($row = $res->fetch_assoc()) {
    print_r($row);
}

?>
1 Like

Eu folosesc ceva de genu, ca si exemplu

$argsPost = array(
            'iId' => FILTER_SANITIZE_NUMBER_INT,
            'action' => FILTER_SANITIZE_STRING,
            'descr' => array(
                'filter' => FILTER_SANITIZE_STRING,
                'flags' => FILTER_REQUIRE_ARRAY
            )

        );
        $this->post = filter_input_array(INPUT_POST, $argsPost);
2 Likes