Come posso prevenire l'SQL injection in PHP?


Answers

Attenzione: il codice di esempio di questa risposta (come il codice di esempio della domanda) utilizza l'estensione mysql di PHP, che è stata deprecata in PHP 5.5.0 e completamente rimossa in PHP 7.0.0.

Se stai usando una versione recente di PHP, l'opzione mysql_real_escape_string descritta di seguito non sarà più disponibile (sebbene mysqli::escape_string sia un equivalente moderno). In questi giorni l'opzione mysql_real_escape_string avrebbe senso solo per il codice legacy su una vecchia versione di PHP.

Hai due opzioni: sfuggire i caratteri speciali nella unsafe_variable o utilizzare una query con parametri. Entrambi ti proteggeranno dall'iniezione SQL. La query parametrizzata è considerata la migliore pratica, ma prima di poter essere utilizzata è necessario passare a una nuova estensione mysql in PHP.

Tratteremo prima la stringa di impatto inferiore che sfugge a una.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Vedi anche, i dettagli della funzione mysql_real_escape_string .

Per utilizzare la query parametrizzata, è necessario utilizzare MySQLi piuttosto che le funzioni MySQL . Per riscrivere il tuo esempio, avremmo bisogno di qualcosa di simile al seguente.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

La funzione chiave su cui vorresti leggere sarebbe mysqli::prepare .

Inoltre, come altri hanno suggerito, potresti trovare utile / più facile aumentare uno strato di astrazione con qualcosa come PDO .

Tieni presente che il caso che hai chiesto è abbastanza semplice e che casi più complessi potrebbero richiedere approcci più complessi. In particolare:

  • Se si desidera modificare la struttura di SQL in base all'input dell'utente, le query con parametri non saranno di aiuto e l'escape richiesto non è coperto da mysql_real_escape_string . In questo tipo di casi, sarebbe meglio passare l'input dell'utente attraverso una whitelist per garantire che solo i valori "sicuri" siano consentiti.
  • Se si utilizzano numeri interi da input dell'utente in una condizione e si utilizza l'approccio mysql_real_escape_string , si verificherà il problema descritto da Polynomial nei commenti seguenti. Questo caso è più complicato perché gli interi non sono racchiusi tra virgolette, quindi è possibile gestirli convalidando che l'input dell'utente contenga solo cifre.
  • Ci sono probabilmente altri casi di cui non sono a conoscenza. Potresti scoprire che this è una risorsa utile su alcuni dei problemi più sottili che puoi incontrare.
Question

Se l'input dell'utente viene inserito senza modifiche in una query SQL, l'applicazione diventa vulnerabile all'iniezione SQL , come nell'esempio seguente:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

Questo perché l'utente può inserire qualcosa come value'); DROP TABLE table;-- value'); DROP TABLE table;-- , e la query diventa:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Cosa si può fare per evitare che ciò accada?




For those unsure of how to use PDO (coming from the mysql_ functions), I made a very, very simple PDO wrapper that is a single file. It exists to show how easy it is to do all the common things applications need to be done. Works with PostgreSQL, MySQL, and SQLite.

Basically, read it PDO to see how to put the PDO functions to use in real life to make it simple to store and retrieve values in the format you want.

I want a single column

$count = DB::column('SELECT COUNT(*) FROM `user`);

I want an array(key => value) results (ie for making a selectbox)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

I want a single row result

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

I want an array of results

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));



La query parametrizzata e la convalida dell'input sono la strada da seguire. Esistono molti scenari in cui è possibile che si verifichi l'iniezione SQL, anche se è stato utilizzato mysql_real_escape_string() .

Questi esempi sono vulnerabili all'iniezione SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

o

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

In entrambi i casi, non è possibile utilizzare ' per proteggere l'incapsulamento.

Source : Inaspettata SQL Injection (quando la fuga non è sufficiente)




Come puoi vedere, le persone suggeriscono di utilizzare al massimo dichiarazioni preparate. Non è sbagliato, ma quando la tua query viene eseguita solo una volta per processo, ci sarà una leggera penalizzazione delle prestazioni.

Stavo affrontando questo problema, ma penso di averlo risolto in modo molto sofisticato - il modo in cui gli hacker usano per evitare di usare le virgolette. L'ho usato in congiunzione con dichiarazioni preparate emulate. Lo uso per prevenire tutti i tipi di possibili attacchi SQL injection.

Il mio approccio:

  • Se ti aspetti che l'input sia intero, assicurati che sia veramente intero. In un linguaggio di tipo variabile come PHP è molto importante. Ad esempio, è possibile utilizzare questa soluzione molto semplice ma potente: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • Se ti aspetti altro dall'esempio intero lo esagero . Se lo esagoni, sfuggirai perfettamente a tutti gli input. In C / C ++ c'è una funzione chiamata mysql_hex_string() , in PHP puoi usare bin2hex() .

    Non preoccuparti che la stringa con escape avrà una dimensione 2x della sua lunghezza originale perché anche se usi mysql_real_escape_string , PHP deve allocare la stessa capacità ((2*input_length)+1) , che è lo stesso.

  • Questo metodo esadecimale viene spesso utilizzato quando si trasferiscono dati binari, ma non vedo alcun motivo per non utilizzarlo su tutti i dati per evitare attacchi di SQL injection. Nota che devi anteporre i dati con 0x o usare invece la funzione MySQL UNHEX .

Quindi, ad esempio, la query:

SELECT password FROM users WHERE name = 'root'

Diventerà:

SELECT password FROM users WHERE name = 0x726f6f74

o

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex è la fuga perfetta. Nessun modo di iniettare.

Differenza tra la funzione UNHEX e il prefisso 0x

C'è stata una discussione nei commenti, quindi alla fine voglio chiarire. Questi due approcci sono molto simili, ma in qualche modo sono leggermente diversi:

Il prefisso ** 0x ** può essere utilizzato solo per colonne di dati come char, varchar, text, block, binary, ecc .
Inoltre, il suo uso è un po 'complicato se stai per inserire una stringa vuota. Dovrai sostituirlo interamente con '' o avrai un errore.

UNHEX () funziona su qualsiasi colonna; non devi preoccuparti della stringa vuota.

I metodi esadecimali sono spesso usati come attacchi

Si noti che questo metodo esadecimale viene spesso utilizzato come attacco di iniezione SQL in cui gli interi sono come le stringhe e sono fuggiti solo con mysql_real_escape_string . Quindi puoi evitare l'uso di virgolette.

Ad esempio, se fai solo qualcosa del genere:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

un attacco può iniettarti molto facilmente . Considera il seguente codice iniettato restituito dallo script:

SELEZIONA ... WHERE id = -1 union seleziona select table_name da information_schema.tables

e ora basta estrarre la struttura della tabella:

SELEZIONA ... WHERE id = -1 unione seleziona select nome_colonna da information_schema.column dove table_name = 0x61727469636c65

E poi seleziona solo i dati che vuoi. Non è bello?

Ma se il programmatore di un sito iniettabile lo esageri, nessuna iniezione sarebbe possibile perché la query sarebbe simile a questa: SELECT ... WHERE id = UNHEX('2d312075...3635')




A good idea is to use an 'object-relational mapper' like Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

It not only saves you from SQL injections but from syntax errors too! Also Supports collections of models with method chaining to filter or apply actions to multiple results at once and multiple connections.




A simple way would be to use a PHP framework like CodeIgniter or Laravel which have inbuilt features like filtering and active-record so that you don't have to worry about these nuances.




Consiglio di utilizzare PDO (PHP Data Objects) per eseguire query SQL con parametri.

Non solo protegge da SQL injection, ma velocizza anche le query.

E usando le mysql_ PDO piuttosto che mysql_ , mysqli_ e pgsql_ , si rende l'app un po 'più astratta dal database, nella rara eventualità che si debba cambiare fornitore di database.




I favor stored procedures ( MySQL has had stored procedures support since 5.0 ) from a security point of view - the advantages are -

  1. Most databases (including MySQL ) enable user access to be restricted to executing stored procedures. The fine-grained security access control is useful to prevent escalation of privileges attacks. This prevents compromised applications from being able to run SQL directly against the database.
  2. They abstract the raw SQL query from the application so less information of the database structure is available to the application. This makes it harder for people to understand the underlying structure of the database and design suitable attacks.
  3. They accept only parameters, so the advantages of parameterized queries are there. Of course - IMO you still need to sanitize your input - especially if you are using dynamic SQL inside the stored procedure.

The disadvantages are -

  1. They (stored procedures) are tough to maintain and tend to multiply very quickly. This makes managing them an issue.
  2. They are not very suitable for dynamic queries - if they are built to accept dynamic code as parameters then a lot of the advantages are negated.



The simple alternative to this problem could be solved by granting appropriate permissions in the database itself. For example: if you are using a mysql database then enter into the database through terminal or the UI provided and just follow this command:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

This will restrict the user to only get confined with the specified query's only. Remove the delete permission and so the data would never get deleted from the query fired from the php page. The second thing to do is to flush the privileges so that the mysql refreshes the permissions and updates.

FLUSH PRIVILEGES; 

more information about flush .

To see the current privileges for the user fire the following query.

select * from mysql.user where User='username';

Learn more about GRANT .




Avviso di sicurezza : questa risposta non è in linea con le migliori pratiche di sicurezza. L'escaping è inadeguato per prevenire l'iniezione SQL , utilizzare invece istruzioni preparate . Utilizzare la strategia descritta di seguito a proprio rischio. (Inoltre, mysql_real_escape_string() stato rimosso in PHP 7.)

Potresti fare qualcosa di base come questo:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Questo non risolverà tutti i problemi, ma è un ottimo trampolino di lancio. Ho omesso elementi ovvi come controllare l'esistenza della variabile, il formato (numeri, lettere, ecc.).




I think if someone wants to use PHP and MySQL or some other dataBase server:

  1. Think about learning PDO (PHP Data Objects) – it is a database access layer providing a uniform method of access to multiple databases.
  2. Think about learning MySQLi
  3. Use native PHP functions like: strip_tags , mysql_real_escape_string() or if variable numeric, just (int)$foo . Read more about type of variables in PHP here . If you're using libraries such as PDO or MySQLi, always use PDO::quote() and mysqli_real_escape_string() .

Libraries examples:

---- PDO

----- No placeholders - ripe for SQL injection! It's bad

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Unnamed placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Named placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PS :

PDO wins this battle with ease. With support for twelve different database drivers and named parameters, we can ignore the small performance loss, and get used to its API. From a security standpoint, both of them are safe as long as the developer uses them the way they are supposed to be used

But while both PDO and MySQLi are quite fast, MySQLi performs insignificantly faster in benchmarks – ~2.5% for non-prepared statements, and ~6.5% for prepared ones.

And please test every query to your database - it's a better way to prevent injection.




I've written this little function several years ago:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

This allows running statements in an one-liner C#-ish String.Format like:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

It escapes considering the variable type. If you try to parameterize table, column names, it would fail as it puts every string in quotes which is an invalid syntax.

AGGIORNAMENTO DELLA SICUREZZA: la str_replaceversione precedente consentiva le iniezioni aggiungendo {#} token ai dati dell'utente. Questa preg_replace_callbackversione non causa problemi se la sostituzione contiene questi token.




** Warning: the approach described in this answer only applies to very specific scenarios and isn't secure since SQL injection attacks do not only rely on being able to inject X=Y .**

If the attackers are trying to hack into the form via PHP's $_GET variable or with the URL's query string, you would be able to catch them if they're not secure.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Because 1=1 , 2=2 , 1=2 , 2=1 , 1+1=2 , etc... are the common questions to an SQL database of an attacker. Maybe also it's used by many hacking applications.

But you must be careful, that you must not rewrite a safe query from your site. The code above is giving you a tip, to rewrite or redirect (it depends on you) that hacking-specific dynamic query string into a page that will store the attacker's IP address , or EVEN THEIR COOKIES, history, browser, or any other sensitive information, so you can deal with them later by banning their account or contacting authorities.