mysql how to - Come posso prevenire l'SQL injection in PHP?





14 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 come il 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.
make input example

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?




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.




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')




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.).




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)




Preferisco le stored procedure ( MySQL ha avuto il supporto delle stored procedure da 5.0 ) da un punto di vista della sicurezza - i vantaggi sono -

  1. La maggior parte dei database (incluso MySQL ) consente di limitare l'accesso degli utenti all'esecuzione di stored procedure. Il controllo dell'accesso di sicurezza a grana fine è utile per prevenire l'escalation degli attacchi con privilegi. Ciò impedisce alle applicazioni compromesse di essere in grado di eseguire SQL direttamente sul database.
  2. Essi astraggono la query SQL raw dall'applicazione in modo da rendere disponibili all'applicazione meno informazioni sulla struttura del database. Ciò rende più difficile per le persone comprendere la struttura sottostante del database e progettare attacchi appropriati.
  3. Accettano solo parametri, quindi i vantaggi delle query parametrizzate sono lì. Ovviamente, IMO è ancora necessario disinfettare il tuo input, specialmente se stai utilizzando SQL dinamico all'interno della stored procedure.

Gli svantaggi sono -

  1. Loro (procedure memorizzate) sono difficili da mantenere e tendono a moltiplicarsi molto rapidamente. Questo rende la gestione di un problema.
  2. Non sono molto adatti per le query dinamiche - se sono costruiti per accettare il codice dinamico come parametri, molti dei vantaggi vengono annullati.



Penso che se qualcuno vuole usare PHP e MySQL o qualche altro server dataBase:

  1. Pensa all'apprendimento di PDO (PHP Data Objects): si tratta di un livello di accesso al database che fornisce un metodo uniforme di accesso a più database.
  2. Pensa all'apprendimento di MySQLi
  3. Usa le funzioni native di PHP come: strip_tags , mysql_real_escape_string() o se variabile numerica, solo (int)$foo. Leggi di più sul tipo di variabili in PHP here . Se stai usando librerie come PDO o MySQLi, usa sempre PDO::quote() e mysqli_real_escape_string() .

Esempi di librerie:

---- DOP

----- Nessun segnaposto - maturo per l'iniezione SQL! È cattivo

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

----- Segnaposto senza nome

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

----- Segnaposto con nome

$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 :

DOP vince questa battaglia con facilità. Con il supporto di dodici diversi driver di database e parametri con nome, possiamo ignorare la piccola perdita di prestazioni e abituarci alla sua API. Dal punto di vista della sicurezza, entrambi sono sicuri fino a quando lo sviluppatore li usa come dovrebbero essere usati

Ma mentre sia PDO che MySQLi sono piuttosto veloci, MySQLi ha prestazioni insignificanti più veloci nei benchmark: ~ 2,5% per le dichiarazioni non preparate e ~ 6,5% per quelle preparate.

E per favore prova ogni query nel tuo database - è un modo migliore per prevenire l'iniezione.







Per chi non è sicuro di come usare PDO (proveniente dalle mysql_funzioni), ho realizzato un wrapper PDO molto, molto semplice, che è un singolo file. Esiste per mostrare quanto sia facile fare tutte le cose comuni che le applicazioni devono essere fatte. Funziona con PostgreSQL, MySQL e SQLite.

In sostanza, leggerlo PDO per vedere come mettere le funzioni DOP da utilizzare nella vita reale per rendere più semplice per archiviare e recuperare i valori nel formato che si desidera.

Voglio una singola colonna

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

Voglio un risultato dell'array (chiave => valore) (es. Per creare una selectbox)

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

Voglio un risultato a riga singola

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

Voglio una serie di risultati

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



La semplice alternativa a questo problema potrebbe essere risolta concedendo autorizzazioni appropriate nel database stesso. Ad esempio: se si utilizza un database mysql, inserire il database tramite terminale o l'interfaccia utente fornita e seguire semplicemente questo comando:

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

Ciò limiterà l'utente a essere limitato solo con la query specificata. Rimuovi l'autorizzazione di eliminazione e quindi i dati non verranno mai eliminati dalla query generata dalla pagina php. La seconda cosa da fare è svuotare i privilegi in modo che mysql aggiorni le autorizzazioni e gli aggiornamenti.

FLUSH PRIVILEGES; 

ulteriori informazioni su flush .

Per vedere i privilegi attuali per l'utente, attiva la seguente query.

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

Ulteriori informazioni su GRANT .




Un modo semplice sarebbe quello di utilizzare un framework PHP come CodeIgniter o Laravel che hanno funzioni integrate come il filtro e la registrazione attiva in modo da non doversi preoccupare di queste sfumature.




** Attenzione: l'approccio descritto in questa risposta si applica solo a scenari molto specifici e non è sicuro poiché gli attacchi di SQL injection non si basano solo sulla possibilità di iniettare X=Y. **

Se gli hacker tentano di hackerare il modulo tramite la $_GETvariabile di PHP o con la stringa di query dell'URL, sarai in grado di catturarli se non sono sicuri.

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

Perché 1=1, 2=2, 1=2, 2=1, 1+1=2, ecc ... sono le domande comuni a un database SQL di un attaccante. Forse è anche usato da molte applicazioni di hacking.

Ma devi stare attento, che non devi riscrivere una query sicura dal tuo sito. Il codice sopra ti dà un consiglio, per riscrivere o reindirizzare (dipende da te) quella stringa di query dinamica specifica per l'hacking in una pagina che memorizzerà l' indirizzo IP dell'utente malintenzionato , ANCORA I LORO COOKIES, la cronologia, il browser o qualsiasi altro elemento sensibile informazioni, in modo da poterle affrontare in seguito mettendo al bando il loro account o contattando le autorità.




Una buona idea è usare un 'mappatore di oggetti relazionale' come 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;
}

Non ti salva solo da iniezioni SQL ma anche da errori di sintassi! Supporta anche raccolte di modelli con concatenamento di metodi per filtrare o applicare azioni a più risultati contemporaneamente e connessioni multiple.




Ho scritto questa piccola funzione diversi anni fa:

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;
}

Ciò consente di eseguire istruzioni in una stringa C # -ish one-liner.Formato come:

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

Fugge considerando il tipo di variabile. Se provate a parametrizzare la tabella, i nomi delle colonne, fallirebbero perché mette ogni stringa tra virgolette che è una sintassi non valida.

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.




Related