Como posso impedir a injeção de SQL em PHP?

2773

Se a entrada do usuário for inserida sem modificação em uma consulta SQL, o aplicativo se tornará vulnerável à injeção de SQL , como no exemplo a seguir:

$unsafe_variable = $_POST['user_input']; 

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

Isso ocorre porque o usuário pode inserir algo como value'); DROP TABLE table;--, e a consulta se torna:

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

O que pode ser feito para impedir que isso aconteça?

Andrew G. Johnson
fonte

Respostas:

8960

Use instruções preparadas e consultas parametrizadas. Estas são instruções SQL enviadas e analisadas pelo servidor de banco de dados separadamente de quaisquer parâmetros. Dessa forma, é impossível para um invasor injetar SQL malicioso.

Você basicamente tem duas opções para conseguir isso:

  1. Usando o PDO (para qualquer driver de banco de dados suportado):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. Usando o MySQLi (para MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // Do something with $row
    }
    

Se você estiver se conectando a um banco de dados que não seja o MySQL, existe uma segunda opção específica do driver à qual você pode se referir (por exemplo, pg_prepare()e pg_execute()no PostgreSQL). O DOP é a opção universal.


Configurando corretamente a conexão

Observe que ao usar PDOpara acessar um banco de dados MySQL, as instruções reais preparadas não são usadas por padrão . Para corrigir isso, você deve desativar a emulação de instruções preparadas. Um exemplo de criação de uma conexão usando o PDO é:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

No exemplo acima, o modo de erro não é estritamente necessário, mas é recomendável adicioná-lo . Dessa forma, o script não irá parar com um Fatal Errorquando algo der errado. E isso dá ao desenvolvedor a chance de catchqualquer erro que seja thrown como PDOExceptions.

O que é obrigatório , no entanto, é a primeira setAttribute()linha, que informa ao PDO para desativar as instruções preparadas emuladas e usar as declarações reais preparadas. Isso garante que a instrução e os valores não sejam analisados ​​pelo PHP antes de enviá-la ao servidor MySQL (dando a um possível invasor nenhuma chance de injetar SQL malicioso).

Embora você possa definir as charsetopções do construtor, é importante observar que as versões 'mais antigas' do PHP (anteriores à 5.3.6) ignoravam silenciosamente o parâmetro charset no DSN.


Explicação

A instrução SQL para a qual você passa prepareé analisada e compilada pelo servidor de banco de dados. Ao especificar parâmetros (um ?ou um parâmetro nomeado, como :nameno exemplo acima), você informa ao mecanismo do banco de dados onde deseja filtrar. Então, quando você chama execute, a instrução preparada é combinada com os valores dos parâmetros especificados.

O importante aqui é que os valores dos parâmetros são combinados com a instrução compilada, não uma string SQL. A injeção de SQL funciona enganando o script para incluir seqüências maliciosas quando ele cria o SQL para enviar ao banco de dados. Portanto, enviando o SQL real separadamente dos parâmetros, você limita o risco de acabar com algo que não pretendia.

Quaisquer parâmetros que você enviar ao usar uma instrução preparada serão tratados apenas como strings (embora o mecanismo do banco de dados possa fazer alguma otimização, portanto os parâmetros também podem acabar como números, é claro). No exemplo acima, se a $namevariável contiver 'Sarah'; DELETE FROM employeeso resultado, seria simplesmente uma pesquisa para a sequência "'Sarah'; DELETE FROM employees"e você não terminará com uma tabela vazia .

Outro benefício do uso de instruções preparadas é que, se você executar a mesma instrução várias vezes na mesma sessão, ela será analisada e compilada apenas uma vez, fornecendo alguns ganhos de velocidade.

Ah, e desde que você perguntou sobre como fazer isso para uma inserção, aqui está um exemplo (usando o DOP):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

Instruções preparadas podem ser usadas para consultas dinâmicas?

Embora você ainda possa usar instruções preparadas para os parâmetros da consulta, a estrutura da consulta dinâmica em si não pode ser parametrizada e certos recursos da consulta não podem ser parametrizados.

Para esses cenários específicos, a melhor coisa a fazer é usar um filtro de lista de permissões que restrinja os valores possíveis.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}
PeeHaa
fonte
86
Só para acrescentar, porque eu não o vi em nenhum outro lugar aqui, outra linha de defesa é um firewall de aplicativo da web (WAF) que pode ter regras definidas para procurar ataques de injeção de sql:
jkerak
37
Além disso, a documentação oficial do mysql_query permite apenas executar uma consulta, portanto qualquer outra consulta além; é ignorado. Mesmo se isso já estiver obsoleto, existem muitos sistemas no PHP 5.5.0 e que podem usar esta função. php.net/manual/en/function.mysql-query.php
Randall Valenciano
12
Esse é um mau hábito, mas é uma solução pós-problema: não apenas para injeção de SQL, mas para qualquer tipo de injeção (por exemplo, houve um furo de injeção de modelo de exibição no F3 framework v2) se você tiver um site ou aplicativo antigo pronto a partir de defeitos de injeção, uma solução é reatribuir os valores de seus vars predefinidos superslobais como $ _POST com valores de escape no bootstrap. Pelo PDO, ainda é possível escapar (também para os quadros de hoje): substr ($ pdo-> quote ($ str, \ PDO :: PARAM_STR), 1, -1)
Alix
15
Essa resposta não possui a explicação do que é uma declaração preparada - uma coisa - é um impacto no desempenho se você usar muitas declarações preparadas durante sua solicitação e, às vezes, é responsável por um impacto 10x no desempenho. O melhor seria usar o PDO com a ligação de parâmetro desativada, mas a preparação da instrução desativada.
donis
6
Usando DOP é melhor, no caso de você estiver usando make consulta direta se você usar mysqli :: escape_string
Kassem Itani
1652

Aviso preterido: O código de exemplo desta resposta (como o código de exemplo da pergunta) usa a MySQLextensão do PHP , que foi preterida no PHP 5.5.0 e removida totalmente no PHP 7.0.0.

Aviso de segurança : Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para impedir a injeção de SQL ; use instruções preparadas . Use a estratégia descrita abaixo por sua conta e risco. (Além disso, mysql_real_escape_string()foi removido no PHP 7.)

Se você estiver usando uma versão recente do PHP, a mysql_real_escape_stringopção descrita abaixo não estará mais disponível (embora mysqli::escape_stringseja um equivalente moderno). Atualmente, a mysql_real_escape_stringopção só faria sentido para o código legado em uma versão antiga do PHP.


Você tem duas opções: escapar dos caracteres especiais no seu unsafe_variableou usar uma consulta parametrizada. Ambos o protegeriam da injeção de SQL. A consulta parametrizada é considerada a melhor prática, mas exigirá a alteração para uma extensão mais recente do MySQL no PHP antes que você possa usá-la.

Abordaremos a cadeia de menor impacto escapando primeiro.

//Connect

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

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

//Disconnect

Veja também, os detalhes da mysql_real_escape_stringfunção.

Para usar a consulta parametrizada, você precisa usar o MySQLi em vez das funções do MySQL . Para reescrever seu exemplo, precisaríamos de algo como o seguinte.

<?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();
?>

A principal função que você deseja ler seria mysqli::prepare.

Além disso, como outros sugeriram, você pode achar útil / mais fácil aumentar uma camada de abstração com algo como DOP .

Observe que o caso que você perguntou é bastante simples e que casos mais complexos podem exigir abordagens mais complexas. Em particular:

  • Se você deseja alterar a estrutura do SQL com base na entrada do usuário, as consultas parametrizadas não ajudarão e a fuga necessária não será abordada mysql_real_escape_string. Nesse tipo de caso, seria melhor passar a entrada do usuário por uma lista de permissões para garantir que apenas valores "seguros" sejam permitidos.
  • Se você usar números inteiros da entrada do usuário em uma condição e adotar a mysql_real_escape_stringabordagem, sofrerá com o problema descrito por Polynomial nos comentários abaixo. Esse caso é mais complicado, pois os números inteiros não seriam cercados por aspas; portanto, você pode lidar com isso validando que a entrada do usuário contém apenas dígitos.
  • Provavelmente há outros casos que não conheço. Você pode achar que esse é um recurso útil para alguns dos problemas mais sutis que você pode encontrar.
Matt Sheppard
fonte
1
usando mysql_real_escape_stringé suficiente ou devo usar parametrizado também?
peiman F.
5
@peimanF. mantenha uma boa prática de usar consultas parametrizadas, mesmo em um projeto local. Com consultas parametrizadas, você garante que não haverá injeção de SQL. Mas tenha em mente que você deve higienizar os dados para evitar falsos recuperação (ou seja XSS injeção, tais como colocar o código HTML em um texto) com htmlentities, por exemplo
Goufalite
2
@peimanF. Boa prática para consultas parametrizadas e valores de vinculação, mas corda fuga real é bom para agora
Richard
Entendo a inclusão de mysql_real_escape_string()integridade, mas não sou fã de listar primeiro a abordagem mais suscetível a erros. O leitor pode pegar rapidamente o primeiro exemplo. Ainda bem que ele está obsoleta agora :)
Steen Schütt
1
@ SteenSchütt - Todas as mysql_*funções estão obsoletas. Eles foram substituídos por funções semelhantes mysqli_* , como mysqli_real_escape_string.
Rick James
1073

Todas as respostas aqui abrangem apenas parte do problema. De fato, existem quatro partes de consulta diferentes que podemos adicionar dinamicamente ao SQL:

  • uma linha
  • um número
  • um identificador
  • uma palavra-chave de sintaxe

E as declarações preparadas abrangem apenas dois deles.

Mas, às vezes, precisamos tornar nossa consulta ainda mais dinâmica, adicionando operadores ou identificadores também. Portanto, precisaremos de diferentes técnicas de proteção.

Em geral, essa abordagem de proteção é baseada na lista de permissões .

Nesse caso, todos os parâmetros dinâmicos devem ser codificados no seu script e escolhidos a partir desse conjunto. Por exemplo, para fazer pedidos dinâmicos:

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

Para facilitar o processo, escrevi uma função auxiliar da lista de permissões que faz todo o trabalho em uma linha:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

Existe outra maneira de proteger identificadores - escapar, mas prefiro a lista de permissões como uma abordagem mais robusta e explícita. No entanto, desde que você tenha um identificador citado, você pode escapar do caractere de citação para torná-lo seguro. Por exemplo, por padrão para o mysql, você deve duplicar o caractere de citação para escapar dele . Para outras outras regras de escape do DBMS, seria diferente.

Ainda assim, há um problema com palavras-chave de sintaxe SQL (como AND, DESCe tal), mas a lista branca parece ser a única abordagem nesse caso.

Portanto, uma recomendação geral pode ser formulada como

  • Qualquer variável que represente um literal de dados SQL (ou, para simplificar - uma sequência SQL ou um número) deve ser adicionada por meio de uma instrução preparada. Sem exceções.
  • Qualquer outra parte da consulta, como uma palavra-chave SQL, uma tabela ou um nome de campo ou um operador - deve ser filtrada por uma lista branca.

Atualizar

Embora exista um acordo geral sobre as práticas recomendadas em relação à proteção contra injeção de SQL, ainda existem muitas práticas inadequadas. E alguns deles estão profundamente enraizados nas mentes dos usuários de PHP. Por exemplo, nesta página, existem (embora invisíveis para a maioria dos visitantes) mais de 80 respostas excluídas - todas removidas pela comunidade devido à má qualidade ou à promoção de práticas ruins e desatualizadas. Pior ainda, algumas das respostas ruins não são excluídas, mas sim prósperas.

Por exemplo, (1) existem (2) ainda (3) muitas (4) respostas (5) , incluindo a segunda resposta mais votada, sugerindo que você escape manualmente de strings - uma abordagem desatualizada que é comprovadamente insegura.

Ou há uma resposta um pouco melhor que sugere apenas outro método de formatação de string e até se orgulha de ser a panacéia definitiva. Embora, claro, não seja. Esse método não é melhor do que a formatação regular de strings, mas mantém todos os seus inconvenientes: é aplicável apenas a strings e, como qualquer outra formatação manual, é essencialmente uma medida opcional e não obrigatória, propensa a erros humanos de qualquer espécie.

Eu acho que tudo isso por causa de uma superstição muito antiga, apoiada por autoridades como o OWASP ou o manual do PHP , que proclama a igualdade entre qualquer "escape" e proteção contra injeções de SQL.

Independentemente do que o manual do PHP disser há anos, *_escape_stringde maneira alguma torna os dados seguros e nunca foram planejados. Além de ser inútil para qualquer parte do SQL que não seja uma string, o escape manual está errado, porque é manual e oposto ao automatizado.

E o OWASP piora ainda mais, enfatizando a fuga da entrada do usuário, o que é um absurdo absoluto: não deve haver tais palavras no contexto da proteção contra injeção. Toda variável é potencialmente perigosa - não importa a fonte! Ou, em outras palavras - todas as variáveis ​​precisam ser formatadas corretamente para serem colocadas em uma consulta - não importa a fonte novamente. É o destino que importa. No momento em que um desenvolvedor começa a separar as ovelhas das cabras (pensando se alguma variável em particular é "segura" ou não), ele / ela dá seu primeiro passo em direção ao desastre. Sem mencionar que mesmo o texto sugere uma fuga em massa no ponto de entrada, semelhante ao recurso de citações mágicas - já desprezado, reprovado e removido.

Portanto, diferente de qualquer "escape", as instruções preparadas são a medida que realmente protege da injeção de SQL (quando aplicável).

Seu senso comum
fonte
848

Eu recomendo usar o PDO (PHP Data Objects) para executar consultas SQL parametrizadas.

Isso não apenas protege contra a injeção de SQL, mas também acelera as consultas.

E, usando o PDO em vez de mysql_, mysqli_e pgsql_funções, você torna seu aplicativo um pouco mais abstrato do banco de dados, na rara ocorrência de que você precisa trocar de provedor de banco de dados.

Kibbee
fonte
619

Use PDOe prepare consultas.

( $conné um PDOobjeto)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
Imran
fonte
549

Como você pode ver, as pessoas sugerem que você use no máximo declarações preparadas. Não está errado, mas quando sua consulta é executada apenas uma vez por processo, haverá uma pequena penalidade no desempenho.

Eu estava enfrentando esse problema, mas acho que o resolvi de maneira muito sofisticada - a maneira como os hackers usam para evitar o uso de aspas. Eu usei isso em conjunto com instruções preparadas emuladas. Eu o uso para evitar todos os tipos de possíveis ataques de injeção de SQL.

Minha abordagem:

  • Se você espera que a entrada seja inteira, verifique se ela é realmente inteira. Em uma linguagem de tipo variável como PHP, isso é muito importante. Você pode usar, por exemplo, esta solução muito simples, mas poderosa:sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • Se você espera mais alguma coisa do número inteiro, hexa-o . Se você o enfeitiçar, você escapará perfeitamente de todas as entradas. No C / C ++, há uma função chamada mysql_hex_string(), no PHP você pode usar bin2hex().

    Não se preocupe com o fato de a string de escape ter um tamanho 2x do seu comprimento original, porque mesmo se você usar mysql_real_escape_string, o PHP precisará alocar a mesma capacidade ((2*input_length)+1), que é a mesma.

  • Esse método hexadecimal é frequentemente usado quando você transfere dados binários, mas não vejo razão para não usá-lo em todos os dados para impedir ataques de injeção de SQL. Observe que você precisa acrescentar dados previamente 0xou usar a função MySQL UNHEX.

Então, por exemplo, a consulta:

SELECT password FROM users WHERE name = 'root'

Se tornará:

SELECT password FROM users WHERE name = 0x726f6f74

ou

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

Hex é a fuga perfeita. Não há como injetar.

Diferença entre a função UNHEX e o prefixo 0x

Houve alguma discussão nos comentários, então eu finalmente quero deixar claro. Essas duas abordagens são muito semelhantes, mas são um pouco diferentes em alguns aspectos:

O ** ** 0x prefixo só pode ser utilizado para as colunas de dados, tais como CHAR, VARCHAR, texto, bloco, binário, etc .
Além disso, seu uso é um pouco complicado se você estiver prestes a inserir uma string vazia. Você precisará substituí-lo totalmente ''ou receberá um erro.

UNHEX () funciona em qualquer coluna; você não precisa se preocupar com a string vazia.


Métodos hexadecimais são frequentemente usados ​​como ataques

Observe que esse método hexadecimal é frequentemente usado como um ataque de injeção SQL, onde números inteiros são exatamente como strings e escapam apenas com ele mysql_real_escape_string. Então você pode evitar o uso de aspas.

Por exemplo, se você apenas fizer algo assim:

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

um ataque pode injetá-lo com muita facilidade . Considere o seguinte código injetado retornado do seu script:

SELECT ... WHERE id = -1 une todos selecione table_name em information_schema.tables

e agora apenas extraia a estrutura da tabela:

SELECT ... WHERE id = -1 union all seleciona column_name de information_schema.column em que table_name = 0x61727469636c65

E, em seguida, basta selecionar os dados desejados. Não é legal?

Porém, se o codificador de um site injetável o encurtasse, nenhuma injeção seria possível porque a consulta teria a seguinte aparência: SELECT ... WHERE id = UNHEX('2d312075...3635')

Zaffy
fonte
6
@SumitGupta Sim, você fez. O MySQL não concatena com +mas com CONCAT. E para o desempenho: Eu não acho que afeta o desempenho porque o MySQL tem que analisar dados e não importa se a origem é string ou hex
Zaffy
11
@YourCommonSense Você não entende o conceito ... Se você deseja ter uma string no mysql, cite-a dessa maneira 'root'ou você pode enfeitiçar, 0x726f6f74mas se quiser um número e enviá-lo como string, provavelmente escreverá '42' e não CHAR (42). ) ... '42' em hexadecimal seria 0x3432não0x42
Zaffy
7
@YourCommonSense Não tenho nada a dizer ... apenas lol ... se você ainda quiser experimentar hexadecimal em campos numéricos, consulte o segundo comentário. Aposto com você que vai funcionar.
Zaffy
2
A resposta afirma claramente que não funcionará dessa maneira com valores inteiros, o motivo é que bin2hex converte o valor passado em uma string (e, portanto, bin2hex (0) é 0x30 e não 0x03) - essa é provavelmente a parte que o confunde. . Se você seguir isso, ele funcionará perfeitamente (pelo menos no meu site, testado com 4 versões diferentes do mysql em máquinas debian, 5.1.x a 5.6.x). Afinal, hexadecimal é apenas a forma de representação, não o valor;)
grifo
5
@YourCommonSense você ainda não entende? Você não pode usar 0x e concat porque se a sequência estiver vazia, você terminará com um erro. Se você quiser uma alternativa simples para a sua consulta, tente estaSELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
#
499

Aviso preterido: O código de exemplo desta resposta (como o código de exemplo da pergunta) usa a MySQLextensão do PHP , que foi preterida no PHP 5.5.0 e removida totalmente no PHP 7.0.0.

Aviso de segurança : Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para impedir a injeção de SQL ; use instruções preparadas . Use a estratégia descrita abaixo por sua conta e risco. (Além disso, mysql_real_escape_string()foi removido no PHP 7.)

IMPORTANTE

A melhor maneira de impedir a injeção de SQL é usar instruções preparadas em vez de escapar , como demonstra a resposta aceita .

Existem bibliotecas como Aura.Sql e EasyDB que permitem que os desenvolvedores usem instruções preparadas com mais facilidade. Para saber mais sobre por que as instruções preparadas são melhores para interromper a injeção de SQL , consulte este mysql_real_escape_string()desvio e as vulnerabilidades de injeção de Unicode SQL recentemente corrigidas no WordPress .

Prevenção de injeção - mysql_real_escape_string ()

O PHP possui uma função especialmente criada para evitar esses ataques. Tudo que você precisa fazer é usar a boca cheia de uma função mysql_real_escape_string,.

mysql_real_escape_stringpega uma string que será usada em uma consulta MySQL e retorna a mesma string com todas as tentativas de injeção de SQL escapadas com segurança. Basicamente, ele substituirá as aspas problemáticas (') que um usuário pode digitar por um substituto seguro para MySQL, uma aspas escapadas \'.

NOTA: você deve estar conectado ao banco de dados para usar esta função!

// Conecte-se ao MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

Você pode encontrar mais detalhes no MySQL - SQL Injection Prevention .

rahularyansharma
fonte
30
É o melhor que você pode fazer com a extensão do mysql legado. Para um novo código, é recomendável mudar para mysqli ou PDO.
Álvaro González
7
Não estou de acordo com essa "função criada especialmente para evitar esses ataques". Eu acho que esse mysql_real_escape_stringobjetivo está em permitir construir consulta SQL correta para cada string de dados de entrada. A prevenção de injeção de sql é o efeito colateral dessa função.
Sectus
4
você não usa funções para escrever strings de dados de entrada corretas. Você acabou de escrever os corretos que não precisam ser escapados ou que já foram escapados. mysql_real_escape_string () pode ter sido projetado com o objetivo que você mencionou, mas seu único valor é impedir a injeção.
Nazca
17
ATENÇÃO! mysql_real_escape_string() não é infalível .
eggyal
9
mysql_real_escape_stringagora está obsoleto, portanto, não é mais uma opção viável. Ele será removido no futuro do PHP. É melhor seguir o que o pessoal do PHP ou MySQL recomenda.
JWW
462

Você poderia fazer algo básico assim:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Isso não resolverá todos os problemas, mas é um trampolim muito bom. Eu deixei de lado itens óbvios, como verificar a existência, o formato da variável (números, letras etc.).

Kiran Maniya
fonte
28
Eu tentei o seu exemplo e é muito bem trabalho para me.Could você limpar "isso não vai resolver todos os problemas"
Chinook
14
Se você não citar a string, ela ainda é injetável. Tome $q = "SELECT col FROM tbl WHERE x = $safe_var";por exemplo. A configuração $safe_varpara 1 UNION SELECT password FROM usersfunciona neste caso devido à falta de aspas. Também é possível injetar strings na consulta usando CONCATe CHR.
polinomial
4
@ Polinomial Completamente certo, mas eu consideraria isso apenas como uso errado. Contanto que você o use corretamente, ele definitivamente funcionará.
glglgl
22
ATENÇÃO! mysql_real_escape_string() não é infalível .
eggyal
8
mysql_real_escape_stringagora está obsoleto, portanto, não é mais uma opção viável. Ele será removido no futuro do PHP. É melhor seguir o que o pessoal do PHP ou MySQL recomenda.
JWW
380

Tudo o que você acabar usando, verifique se sua entrada já não foi destruída por magic_quotesalgum lixo bem intencionado e, se necessário, execute-a stripslashesou o que for para higienizá-la.

Rob
fonte
11
De fato; executar com magic_quotes ativado apenas incentiva práticas inadequadas. No entanto, às vezes nem sempre é possível controlar o ambiente nesse nível - ou você não tem acesso para gerenciar o servidor ou seu aplicativo precisa coexistir com aplicativos que (estremecem) dependem dessa configuração. Por esses motivos, é bom escrever aplicativos portáteis - embora, obviamente, o esforço seja desperdiçado se você controlar o ambiente de implantação, por exemplo, porque é um aplicativo interno ou apenas será usado em seu ambiente específico.
24411 Rob
24
A partir do PHP 5.4, a abominação conhecida como 'citações mágicas' foi morta . E boa viagem para lixo ruim.
BryanH
363

Aviso preterido: O código de exemplo desta resposta (como o código de exemplo da pergunta) usa a MySQLextensão do PHP , que foi preterida no PHP 5.5.0 e removida totalmente no PHP 7.0.0.

Aviso de segurança : Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para impedir a injeção de SQL ; use instruções preparadas . Use a estratégia descrita abaixo por sua conta e risco. (Além disso, mysql_real_escape_string()foi removido no PHP 7.)

Consulta parametrizada E validação de entrada é o caminho a percorrer. Existem muitos cenários em que a injeção de SQL pode ocorrer, mesmo que mysql_real_escape_string()tenha sido usada.

Esses exemplos são vulneráveis ​​à injeção de 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");

ou

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

Nos dois casos, você não pode usar 'para proteger o encapsulamento.

Fonte : A injeção inesperada de SQL (quando o escape não é suficiente)

Cedric
fonte
2
Você pode impedir a injeção de SQL se adotar uma técnica de validação de entrada na qual a entrada do usuário seja autenticada em um conjunto de regras definidas para comprimento, tipo e sintaxe, e também nas regras de negócios.
Josip Ivic
312

Na minha opinião, a melhor maneira de impedir geralmente a injeção de SQL no seu aplicativo PHP (ou em qualquer aplicativo da Web) é pensar na arquitetura do seu aplicativo. Se a única maneira de se proteger contra a injeção de SQL é se lembrar de usar um método ou função especial que faça a coisa certa toda vez que você conversar com o banco de dados, estará fazendo errado. Dessa forma, é apenas uma questão de tempo até que você esqueça de formatar corretamente sua consulta em algum momento do seu código.

Adotar o padrão MVC e uma estrutura como CakePHP ou CodeIgniter é provavelmente o caminho a seguir: Tarefas comuns, como a criação de consultas seguras ao banco de dados, foram resolvidas e implementadas centralmente nessas estruturas. Eles ajudam a organizar seu aplicativo da Web de maneira sensata e fazem você pensar mais em carregar e salvar objetos do que em construir com segurança consultas SQL únicas.

Johannes Fahrenkrug
fonte
5
Eu acho que o seu primeiro parágrafo é importante. Compreender é a chave. Além disso, nem todo mundo está trabalhando para uma empresa. Para uma grande quantidade de pessoas, estruturas na verdade vão contra a idéia de entendimento . Ficar íntimo com os fundamentos pode não ser valorizado enquanto trabalha dentro de um prazo, mas os praticantes de bricolage por aí gostam de sujar as mãos. Os desenvolvedores de frameworks não são tão privilegiados que todos os outros devem se curvar e assumir que nunca cometem erros. O poder de tomar decisões ainda é importante. Quem pode dizer que minha estrutura não substituirá outro esquema no futuro?
Anthony Rutledge
@AnthonyRutledge Você está absolutamente correto. É muito importante entender o que está acontecendo e por quê. No entanto, a chance de uma estrutura verdadeira e testada e ativamente usada e desenvolvida ter encontrado e solucionado muitos problemas e corrigido muitas falhas de segurança já é bastante alta. É uma boa ideia olhar a fonte para ter uma idéia da qualidade do código. Se é uma bagunça não testada, provavelmente não é seguro.
Johannes Fahrenkrug
3
Aqui. Aqui. Bons pontos. No entanto, você concorda que muitas pessoas podem estudar e aprender a adotar um sistema MVC, mas nem todos podem reproduzi-lo manualmente (controladores e servidor). Pode-se ir longe demais com esse ponto. Preciso entender meu microondas antes de aquecer meus biscoitos de manteiga de amendoim e pecã que minha namorada me fez? ;-)
Anthony Rutledge
3
@AnthonyRutledge Concordo! Acho que o caso de uso também faz a diferença: estou construindo uma galeria de fotos para minha página pessoal ou estou construindo um aplicativo da web de banco on-line? Neste último caso, é muito importante entender os detalhes de segurança e como uma estrutura que estou usando está lidando com eles.
Johannes Fahrenkrug
3
Ah, a exceção de segurança ao corolário faça você mesmo. Veja, eu tenho tendência a arriscar tudo e ir à falência. :-) Brincando. Com tempo suficiente, as pessoas podem aprender a criar um aplicativo bastante seguro. Muitas pessoas estão com pressa. Eles levantam as mãos e assumem que as estruturas são mais seguras . Afinal, eles não têm tempo suficiente para testar e descobrir as coisas. Além disso, a segurança é um campo que requer estudo dedicado. Não é algo que meros programadores conheçam profundamente em virtude da compreensão de algoritmos e padrões de design.
Anthony Rutledge
298

Eu sou a favor de procedimentos armazenados (o MySQL tem suporte a procedimentos armazenados desde o 5.0 ) do ponto de vista da segurança - as vantagens são -

  1. A maioria dos bancos de dados (incluindo MySQL ) permite que o acesso do usuário seja restrito à execução de procedimentos armazenados. O controle de acesso de segurança refinado é útil para impedir a escalada de ataques de privilégios. Isso impede que aplicativos comprometidos possam executar o SQL diretamente no banco de dados.
  2. Eles abstraem a consulta SQL bruta do aplicativo para que menos informações da estrutura do banco de dados estejam disponíveis para o aplicativo. Isso torna mais difícil para as pessoas entenderem a estrutura subjacente do banco de dados e projetarem ataques adequados.
  3. Eles aceitam apenas parâmetros, portanto, as vantagens das consultas parametrizadas existem. Obviamente - na IMO, você ainda precisa higienizar sua entrada - especialmente se estiver usando SQL dinâmico dentro do procedimento armazenado.

As desvantagens são -

  1. Eles (procedimentos armazenados) são difíceis de manter e tendem a se multiplicar muito rapidamente. Isso torna o gerenciamento deles um problema.
  2. Eles não são muito adequados para consultas dinâmicas - se eles foram criados para aceitar código dinâmico como parâmetros, muitas das vantagens são negadas.
Nikhil
fonte
297

Existem muitas maneiras de impedir injeções de SQL e outros hacks de SQL. Você pode encontrá-lo facilmente na Internet (Pesquisa do Google). Claro que a DOP é uma das boas soluções. Mas eu gostaria de sugerir uma boa prevenção de links da injeção de SQL.

O que é injeção de SQL e como prevenir

Manual PHP para injeção de SQL

Explicação da Microsoft sobre injeção e prevenção de SQL em PHP

E outros como Prevenir a injeção de SQL com MySQL e PHP .

Agora, por que você precisa impedir que sua consulta seja injetada em SQL?

Gostaria de informar: Por que tentamos impedir a injeção de SQL com um pequeno exemplo abaixo:

Consulta para correspondência de autenticação de login:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

Agora, se alguém (um hacker) colocar

$_POST['email']= admin@emali.com' OR '1=1

e senha qualquer coisa ....

A consulta será analisada no sistema apenas até:

$query="select * from users where email='[email protected]' OR '1=1';

A outra parte será descartada. Então o que vai acontecer? Um usuário não autorizado (hacker) poderá fazer login como administrador sem ter sua senha. Agora, ele / ela pode fazer qualquer coisa que o administrador / pessoa de email possa fazer. Veja, é muito perigoso se a injeção de SQL não for impedida.

Manish Shrivastava
fonte
267

Eu acho que se alguém quiser usar PHP e MySQL ou algum outro servidor de banco de dados:

  1. Pense em aprender o DOP (PHP Data Objects) - é uma camada de acesso ao banco de dados que fornece um método uniforme de acesso a vários bancos de dados.
  2. Pense em aprender o MySQLi
  3. Use funções nativas do PHP como: strip_tags , mysql_real_escape_string ou, se variável numérica, apenas (int)$foo. Leia mais sobre o tipo de variáveis ​​em PHP aqui . Se você estiver usando bibliotecas como PDO ou MySQLi, sempre use PDO :: quote () e mysqli_real_escape_string () .

Exemplos de bibliotecas:

---- DOP

----- Nenhum espaço reservado - pronto para injeção de SQL! É mau

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

----- Espaços reservados sem nome

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

----- Espaços reservados nomeados

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

A DOP vence esta batalha com facilidade. Com suporte para doze drivers de banco de dados diferentes e parâmetros nomeados, podemos ignorar a pequena perda de desempenho e nos acostumar com sua API. Do ponto de vista da segurança, os dois estão seguros desde que o desenvolvedor os use da maneira que deveriam ser usados

Mas, embora o PDO e o MySQLi sejam bastante rápidos, o MySQLi executa insignificantemente mais rápido em benchmarks - ~ 2,5% para declarações não preparadas e ~ 6,5% para declarações preparadas.

E teste todas as consultas ao seu banco de dados - é a melhor maneira de evitar a injeção.

RDK
fonte
que o mysqli está incorreto. O primeiro parâmetro expressa os tipos de dados.
mickmackusa
257

Se possível, lance os tipos de seus parâmetros. Mas só funciona em tipos simples como int, bool e float.

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
devOp
fonte
3
Este é um dos poucos casos em que eu usaria um "valor de escape" em vez de uma declaração preparada. E a conversão de tipo inteiro é extremamente eficiente.
HoldOffHunger 13/03/19
233

Se você quiser aproveitar os mecanismos de cache, como Redis ou Memcached , talvez o DALMP possa ser uma opção. Ele usa MySQLi puro . Verifique isto: Camada de abstração de banco de dados DALMP para MySQL usando PHP.

Além disso, você pode 'preparar' seus argumentos antes de preparar sua consulta, para poder criar consultas dinâmicas e, no final, ter uma consulta de instruções totalmente preparada. Camada de Abstração de Banco de Dados DALMP para MySQL usando PHP.

Peter Mortensen
fonte
224

Para quem não tem certeza de como usar o DOP (proveniente das mysql_funções), criei um invólucro do PDO muito, muito simples, que é um único arquivo. Existe para mostrar como é fácil fazer todas as coisas comuns que os aplicativos precisam ser executados. Funciona com PostgreSQL, MySQL e SQLite.

Basicamente, lê-lo enquanto você ler o manual para ver como colocar as funções de DOP para uso na vida real para torná-lo simples para armazenar e recuperar valores no formato que você quiser.

Eu quero uma coluna

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

Eu quero resultados de uma matriz (chave => valor) (ou seja, para fazer uma caixa de seleção)

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

Eu quero um resultado em uma única linha

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

Eu quero uma matriz de resultados

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

Usando esta função PHP, mysql_escape_string()você pode obter uma boa prevenção de maneira rápida.

Por exemplo:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string - Escapa uma string para uso em um mysql_query

Para mais prevenção, você pode adicionar no final ...

wHERE 1=1   or  LIMIT 1

Finalmente você obtém:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
Nicolas Finelli
fonte
220

Algumas diretrizes para escapar caracteres especiais nas instruções SQL.

Não use MySQL . Esta extensão está obsoleta. Use MySQLi ou PDO .

MySQLi

Para escapar manualmente caracteres especiais em uma string, você pode usar a função mysqli_real_escape_string . A função não funcionará corretamente, a menos que o conjunto de caracteres correto seja definido com mysqli_set_charset .

Exemplo:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

Para escape automático de valores com instruções preparadas, use mysqli_prepare e mysqli_stmt_bind_param em que tipos para as variáveis ​​de ligação correspondentes devem ser fornecidos para uma conversão apropriada:

Exemplo:

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

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

Não importa se você usa instruções preparadas ou mysqli_real_escape_string, sempre precisa saber o tipo de dados de entrada com os quais está trabalhando.

Portanto, se você usar uma instrução preparada, deverá especificar os tipos das variáveis ​​para a mysqli_stmt_bind_paramfunção.

E o uso de mysqli_real_escape_stringé para, como o nome diz, escapar caracteres especiais em uma string, para que não torne inteiros seguros. O objetivo desta função é impedir a quebra de cadeias de caracteres nas instruções SQL e os danos ao banco de dados que isso pode causar. mysqli_real_escape_stringé uma função útil quando usada corretamente, especialmente quando combinada com sprintf.

Exemplo:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647
Danijel
fonte
3
A questão é muito genérica. Algumas ótimas respostas acima, mas a maioria sugere declarações preparadas. O MySQLi async não suporta instruções preparadas, portanto o sprintf parece uma ótima opção para esta situação.
Dustin Graham
183

A alternativa simples para esse problema pode ser resolvida concedendo permissões apropriadas no próprio banco de dados. Por exemplo: se você estiver usando um banco de dados MySQL, entre no banco de dados através do terminal ou da interface do usuário fornecida e siga este comando:

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

Isso restringirá o usuário a ficar confinado apenas com as consultas especificadas. Remova a permissão de exclusão e os dados nunca serão excluídos da consulta acionada na página PHP. A segunda coisa a fazer é liberar os privilégios para que o MySQL atualize as permissões e atualizações.

FLUSH PRIVILEGES; 

mais informações sobre descarga .

Para ver os privilégios atuais do usuário, ative a seguinte consulta.

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

Saiba mais sobre o GRANT .

Apurv Nerlekar
fonte
25
Esta resposta está essencialmente errada , pois não ajuda a impedir uma prevenção de injeção, mas apenas tentando amenizar as consequências. Em vão.
Seu senso comum
1
Certo, ele não fornece uma solução, mas é o que você pode fazer antes da mão para evitar as coisas.
Apurv Nerlekar
1
@Apurv Se meu objetivo é ler informações privadas do seu banco de dados, não ter a permissão DELETE significa nada.
Alex Holsgrove #
1
@AlexHolsgrove: Calma, eu estava apenas sugerindo boas práticas para amenizar as consequências.
Apurv Nerlekar
2
@Apurv Você não quer "amenizar as consequências", deseja fazer todo o possível para se proteger. Para ser justo, porém, é importante definir o acesso correto do usuário, mas não é exatamente o que o OP está pedindo.
Alex Holsgrove
177

Em relação a muitas respostas úteis, espero acrescentar algum valor a este tópico.

A injeção de SQL é um ataque que pode ser feito por meio de entradas do usuário (entradas que são preenchidas por um usuário e usadas em consultas). Os padrões de injeção SQL são sintaxe de consulta correta, enquanto podemos chamá-lo: consultas incorretas por motivos inadequados, e assumimos que pode haver uma pessoa má que tenta obter informações secretas (ignorando o controle de acesso) que afetam os três princípios de segurança (confidencialidade). , integridade e disponibilidade).

Agora, nosso objetivo é evitar ameaças à segurança, como ataques de injeção SQL, a pergunta (como evitar um ataque de injeção SQL usando PHP), seja mais realista: filtragem de dados ou limpeza de dados de entrada é o caso ao usar dados de entrada de usuários dentro essa consulta, usando PHP ou qualquer outra linguagem de programação não é o caso, ou conforme recomendado por mais pessoas para usar tecnologia moderna, como declaração preparada ou qualquer outra ferramenta que atualmente suporte a prevenção de injeção de SQL, considera que essas ferramentas não estão mais disponíveis? Como você protege seu aplicativo?

Minha abordagem contra a injeção de SQL é: limpar os dados de entrada do usuário antes de enviá-los ao banco de dados (antes de usá-los em qualquer consulta).

Filtragem de dados para (conversão de dados não seguros em dados seguros)

Considere que o PDO e o MySQLi não estão disponíveis. Como você pode proteger seu aplicativo? Você me força a usá-los? E outras línguas além do PHP? Prefiro fornecer idéias gerais, pois podem ser usadas para bordas mais amplas, não apenas para um idioma específico.

  1. Usuário SQL (limitando o privilégio do usuário): as operações SQL mais comuns são (SELECT, UPDATE, INSERT); por que atribuir o privilégio UPDATE a um usuário que não o exige? Por exemplo, as páginas de login e pesquisa estão usando apenas SELECT; por que usar usuários de banco de dados nessas páginas com altos privilégios?

REGRA: não crie um usuário de banco de dados para todos os privilégios. Para todas as operações SQL, você pode criar seu esquema como (deluser, selectuser, updateuser) como nomes de usuário para facilitar o uso.

Veja o princípio do menor privilégio .

  1. Filtragem de dados: antes de criar qualquer entrada do usuário da consulta, ela deve ser validada e filtrada. Para programadores, é importante definir algumas propriedades para cada variável de entrada do usuário: tipo de dados, padrão de dados e comprimento dos dados . Um campo que é um número entre (x e y) deve ser exatamente validado usando a regra exata, e para um campo que é uma sequência (texto): padrão é o caso, por exemplo, um nome de usuário deve conter apenas alguns caracteres, vamos diga [a-zA-Z0-9_-.]. O comprimento varia entre (x e n) onde x e n (números inteiros, x <= n). Regra: criar filtros exatos e regras de validação são práticas recomendadas para mim.

  2. Use outras ferramentas: Aqui, também concordarei com você que uma declaração preparada (consulta parametrizada) e procedimentos armazenados. As desvantagens aqui são que essas maneiras exigem habilidades avançadas que não existem para a maioria dos usuários. A idéia básica aqui é distinguir entre a consulta SQL e os dados usados ​​dentro. Ambas as abordagens podem ser usadas mesmo com dados não seguros, porque os dados de entrada do usuário aqui não adicionam nada à consulta original, como (any ou x = x).

Para obter mais informações, leia o OWASP SQL Injection Prevention Cheat Sheet .

Agora, se você é um usuário avançado, comece a usar essa defesa como quiser, mas, para iniciantes, se eles não puderem implementar rapidamente um procedimento armazenado e prepararem a instrução, é melhor filtrar os dados de entrada o máximo possível.

Por fim, vamos considerar que um usuário envia este texto abaixo em vez de digitar seu nome de usuário:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

Essa entrada pode ser verificada antecipadamente, sem qualquer declaração preparada e procedimentos armazenados, mas, por segurança, o uso deles é iniciado após a filtragem e validação dos dados do usuário.

O último ponto é detectar comportamentos inesperados, que exigem mais esforço e complexidade; não é recomendado para aplicativos da web normais.

O comportamento inesperado na entrada do usuário acima é SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA e root. Depois que essas palavras forem detectadas, você poderá evitar a entrada.

ATUALIZAÇÃO 1:

Um usuário comentou que esta postagem é inútil, OK! Aqui está o que o OWASP.ORG forneceu :

Defesas primárias:

Opção nº 1: Uso de instruções preparadas (consultas parametrizadas)
Opção nº 2: Uso de procedimentos armazenados
Opção nº 3: Escapando todas as entradas fornecidas pelo usuário

Defesas adicionais:

Aplicar também: Menor privilégio
Executar também: Validação de entrada da lista branca

Como você deve saber, a reivindicação de um artigo deve ser apoiada por um argumento válido, pelo menos por uma referência! Caso contrário, é considerado um ataque e uma reivindicação ruim!

Atualização 2:

No manual do PHP, PHP: Instruções Preparadas - Manual :

Escapamento e injeção de SQL

Variáveis ​​vinculadas serão escapadas automaticamente pelo servidor. O servidor insere seus valores de escape nos locais apropriados no modelo de instrução antes da execução. Uma dica deve ser fornecida ao servidor para o tipo de variável vinculada, para criar uma conversão apropriada. Veja a função mysqli_stmt_bind_param () para mais informações.

O escape automático de valores no servidor às vezes é considerado um recurso de segurança para impedir a injeção de SQL. O mesmo grau de segurança pode ser alcançado com instruções não preparadas se os valores de entrada forem escapados corretamente.

Atualização 3:

Criei casos de teste para saber como o PDO e o MySQLi enviam a consulta ao servidor MySQL ao usar uma instrução preparada:

DOP:

$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

Log de consulta:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

Log de consulta:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

É claro que uma declaração preparada também está escapando dos dados, nada mais.

Como também mencionado na declaração acima,

O escape automático de valores no servidor às vezes é considerado um recurso de segurança para impedir a injeção de SQL. O mesmo grau de segurança pode ser alcançado com instruções não preparadas, se os valores de entrada forem escapados corretamente

Portanto, isso prova que a validação de dados, como intval()é uma boa idéia para valores inteiros, antes de enviar qualquer consulta. Além disso, impedir dados maliciosos do usuário antes de enviar a consulta é uma abordagem correta e válida .

Por favor, veja esta pergunta para mais detalhes: O PDO envia uma consulta bruta ao MySQL enquanto o Mysqli envia uma consulta preparada, ambas produzem o mesmo resultado

Referências:

  1. Folha de dicas sobre injeção de SQL
  2. Injeção SQL
  3. Segurança da informação
  4. Princípios de segurança
  5. Data de validade
user1646111
fonte
175

Aviso de segurança : Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para impedir a injeção de SQL ; use instruções preparadas . Use a estratégia descrita abaixo por sua conta e risco. (Além disso, mysql_real_escape_string()foi removido no PHP 7.)

Aviso obsoleto : A extensão mysql está obsoleta no momento. recomendamos o uso da extensão DOP

Eu uso três maneiras diferentes para impedir que meu aplicativo Web fique vulnerável à injeção de SQL.

  1. Uso de mysql_real_escape_string(), que é uma função pré-definida em PHP , e este código add barras invertidas para os seguintes caracteres: \x00, \n, \r, \, ', "e \x1a. Passe os valores de entrada como parâmetros para minimizar a chance de injeção de SQL.
  2. A maneira mais avançada é usar PDOs.

Eu espero que isso te ajude.

Considere a seguinte consulta:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string () não protegerá aqui. Se você usar aspas simples ('') em torno de suas variáveis ​​na sua consulta, é o que o protege contra isso. Aqui está uma solução abaixo para isso:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

Esta pergunta tem algumas boas respostas sobre isso.

Eu sugiro, usar o DOP é a melhor opção.

Editar:

mysql_real_escape_string()está obsoleto a partir do PHP 5.5.0. Use mysqli ou PDO.

Uma alternativa para mysql_real_escape_string () é

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

Exemplo:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");
Soumalya Banerjee
fonte
169

Uma maneira simples seria usar uma estrutura PHP como CodeIgniter ou Laravel, que possui recursos embutidos como filtragem e registro ativo, para que você não precise se preocupar com essas nuances.

Deepak Thomas
fonte
7
Eu acho que o ponto principal da questão é fazer isso sem o uso dessa estrutura.
Sanke
147

Aviso: a abordagem descrita nesta resposta se aplica apenas a cenários muito específicos e não é segura, pois os ataques de injeção SQL não dependem apenas da capacidade de injetar X=Y.

Se os invasores estiverem tentando invadir o formulário através da $_GETvariável do PHP ou com a string de consulta da URL, você poderá capturá-los se eles não estiverem seguros.

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

Porque 1=1, 2=2, 1=2, 2=1, 1+1=2, etc ... são as perguntas comuns a um banco de dados SQL de um atacante. Talvez também seja usado por muitos aplicativos de hackers.

Mas você deve ter cuidado para não reescrever uma consulta segura em seu site. O código acima fornece uma dica para reescrever ou redirecionar (depende de você) a sequência de consultas dinâmicas específicas de hackers em uma página que armazena o endereço IP do invasor ou até mesmo seus cookies, histórico, navegador ou qualquer outra informação sensível informações, para que você possa lidar com eles posteriormente, banindo a conta ou entrando em contato com as autoridades.

5ervantes
fonte
O que há com isso 1-1=0? :)
Rápli András 01/10/19
@ RápliAndrás Algum tipo de ([0-9\-]+)=([0-9]+).
19/05/19
127

Existem muitas respostas para PHP e MySQL , mas aqui está o código para PHP e Oracle para impedir a injeção de SQL e o uso regular de drivers oci8:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);
Chintan Gor
fonte
Por favor, explique os parâmetros oci_bind_by_name.
Jahanzeb Awan
127

Uma boa idéia é usar um mapeador objeto-relacional como o 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;
}

Ele não apenas economiza injeções de SQL, mas também erros de sintaxe! Ele também suporta coleções de modelos com encadeamento de métodos para filtrar ou aplicar ações a vários resultados ao mesmo tempo e várias conexões.

Thomas Ahle
fonte
124

Aviso preterido: O código de exemplo desta resposta (como o código de exemplo da pergunta) usa a MySQLextensão do PHP , que foi preterida no PHP 5.5.0 e removida totalmente no PHP 7.0.0.

Aviso de segurança : Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para impedir a injeção de SQL ; use instruções preparadas . Use a estratégia descrita abaixo por sua conta e risco. (Além disso, mysql_real_escape_string()foi removido no PHP 7.)

Usar o PDO e o MYSQLi é uma boa prática para evitar injeções de SQL, mas se você realmente deseja trabalhar com funções e consultas do MySQL, seria melhor usar

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

Existem mais habilidades para evitar isso: como identificar - se a entrada é uma string, número, caractere ou matriz, existem muitas funções embutidas para detectar isso. Além disso, seria melhor usar essas funções para verificar os dados de entrada.

is_string

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

E é muito melhor usar essas funções para verificar os dados de entrada mysql_real_escape_string.

Rakesh Sharma
fonte
10
Além disso, não faz sentido verificar os membros da matriz $ _POST com is_string ()
Seu senso comum
21
ATENÇÃO! mysql_real_escape_string() não é infalível .
eggyal
10
mysql_real_escape_stringagora está obsoleto, portanto, não é mais uma opção viável. Ele será removido do PHP no futuro. É melhor seguir o que o pessoal do PHP ou MySQL recomenda.
JWW
2
Tema: Não confie nos dados enviados pelo usuário. Tudo o que você espera são dados de lixo com caracteres especiais ou lógica booleana, que devem tornar-se parte da consulta SQL que você pode estar executando. Mantenha os valores $ _POST como dados apenas, não como parte do SQL.
Bimal Poudel
88

Eu escrevi essa pequena função há vários anos:

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

Isso permite a execução de instruções em uma cadeia de caracteres C # de uma linha.

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

Escapa considerando o tipo de variável. Se você tentar parametrizar nomes de tabelas e colunas, isso falhará, pois coloca todas as seqüências entre aspas, que é uma sintaxe inválida.

ATUALIZAÇÃO DE SEGURANÇA: A str_replaceversão anterior permitia injeções adicionando {#} tokens nos dados do usuário. Esta preg_replace_callbackversão não causa problemas se a substituição contiver esses tokens.

Calmarius
fonte