Pergunta

Se a entrada do usuário for inserida sem modificação em uma consulta SQL, o aplicativo se tornará vulnerável a injeção 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 evitar que isso aconteça?

Foi útil?

Solução

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 DOP (para qualquer driver de banco de dados compatível):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. Usando 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 diferente do MySQL, há uma segunda opção específica do driver que você pode consultar (por exemplo, pg_prepare() e pg_execute() para PostgreSQL).DOP é a opção universal.

Configurando corretamente a conexão

Observe que ao usar PDO para acessar um banco de dados MySQL real declarações preparadas são não usado por padrão.Para corrigir isso você deve desabilitar a emulação de instruções preparadas.Um exemplo de criação de uma conexão usando PDO é:

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

$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 é aconselhável adicioná-lo.Dessa forma, o script não irá parar com um Fatal Error quando algo dá errado.E dá ao desenvolvedor a oportunidade de catch quaisquer erros que sejam thrown como PDOExceptionS.

O que é obrigatório, porém, é o primeiro setAttribute() linha, que diz ao PDO para desabilitar instruções preparadas emuladas e usar real declarações preparadas.Isso garante que a instrução e os valores não sejam analisados ​​pelo PHP antes de enviá-los ao servidor MySQL (dando a um possível invasor nenhuma chance de injetar SQL malicioso).

Embora você possa definir o charset nas opções do construtor, é importante observar que versões 'antigas' do PHP (< ​​5.3.6) ignorou silenciosamente o parâmetro charset no DSN.

Explicação

O que acontece é que a instrução SQL que você passa para prepare é analisado e compilado pelo servidor de banco de dados.Ao especificar parâmetros (um ? ou um parâmetro nomeado como :name no exemplo acima) você informa ao mecanismo de banco de dados onde deseja filtrar.Então quando você ligar execute, a instrução preparada será combinada com os valores de parâmetro especificados.

O importante aqui é que os valores dos parâmetros sejam combinados com a instrução compilada, não com uma string SQL.A injeção de SQL funciona enganando o script para que inclua strings maliciosas ao criar SQL para enviar ao banco de dados.Portanto, ao enviar 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 de banco de dados possa fazer alguma otimização para que os parâmetros também possam acabar como números, é claro).No exemplo acima, se o $name variável contém 'Sarah'; DELETE FROM employees o resultado seria simplesmente uma busca pela string "'Sarah'; DELETE FROM employees", e você não vai acabar com uma mesa vazia.

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

Ah, e já que você perguntou como fazer isso para um insert, aqui vai um exemplo (usando PDO):

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

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

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

Embora você ainda possa usar instruções preparadas para os parâmetros de consulta, a estrutura da consulta dinâmica em si não pode ser parametrizada e determinados recursos de 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';
}

Outras dicas

Aviso obsoleto:O código de exemplo desta resposta (como o código de exemplo da pergunta) usa PHP MySQL extensão, que foi obsoleta 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 evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

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


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

Abordaremos primeiro a corda de menor impacto que escapa de uma.

//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 do mysql_real_escape_string função.

Para usar a consulta parametrizada, você precisa usar MySQLi em vez do MySQL funções.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 função principal 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 vão ajudar e o escape necessário não é coberto por 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 tomar o mysql_real_escape_string abordagem, você sofrerá com o problema descrito por Polinomial nos comentários abaixo.Este caso é mais complicado porque os números inteiros não estariam entre aspas, então você poderia lidar com isso validando que a entrada do usuário contém apenas dígitos.
  • Provavelmente há outros casos dos quais não tenho conhecimento.Você pode encontrar esse é um recurso útil para alguns dos problemas mais sutis que você pode encontrar.

Cada resposta aqui cobre apenas parte do problema.Na verdade, existem quatro diferentes partes da consulta que podemos adicionar dinamicamente:-

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

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

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

Em geral, esta abordagem de protecção baseia-se em lista de permissões.

Nesse caso, cada parâmetro dinâmico deve ser codificado em seu script e escolhido nesse conjunto.Por exemplo, para fazer ordenação dinâmica:

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

No entanto, existe outra maneira de proteger identificadores - escapando.Contanto que você tenha um identificador entre aspas, você pode escapar dos crases duplicando-os.

Como passo adicional, podemos pegar emprestada uma ideia verdadeiramente brilhante de usar algum espaço reservado (um proxy para representar o valor real na consulta) das declarações preparadas e inventar um espaço reservado de outro tipo - um espaço reservado para identificador.

Então, para resumir a longa história:é um espaço reservado, não declaração preparada pode ser considerada uma bala de prata.

Assim, uma recomendação geral pode ser formulada comoContanto que você adicione partes dinâmicas à consulta usando espaços reservados (e esses espaços reservados sejam processados ​​adequadamente, é claro), você pode ter certeza de que sua consulta é segura.

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

Atualizar

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

Por exemplo, lá (1) são (2) ainda (3) muitos(4) respostas (5), incluindo o segunda resposta mais votada sugerindo o escape manual 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.Este método não é melhor que a formatação normal de strings, mas mantém todas as suas desvantagens:é aplicável apenas a strings e, como qualquer outra formatação manual, é essencialmente uma medida opcional e não obrigatória, sujeita a qualquer tipo de erro humano.

Penso que tudo isto por causa de uma superstição muito antiga, apoiada por autoridades como 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 disse há muito tempo, *_escape_string de forma alguma torna os dados seguros e nunca foi pretendido.Além de ser inútil para qualquer parte SQL que não seja string, o escape manual é errado, porque é manual e não automatizado.

E o OWASP torna tudo ainda pior, enfatizando a fuga entrada do usuário o que é um absurdo total:não deveria haver tais palavras no contexto da proteção contra injeção.Cada variável é potencialmente perigosa – não importa a fonte!Ou, em outras palavras - toda variável deve ser formatada corretamente para ser colocada em uma consulta - independentemente da 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 específica é “segura” ou não), ele dá o primeiro passo em direção ao desastre.Sem mencionar que até mesmo o texto sugere escape em massa no ponto de entrada, lembrando o próprio recurso de aspas mágicas - já desprezado, obsoleto e removido.

Então, ao contrário de qualquer declaração preparada que "escapa" é a medida que de fato protege contra injeção de SQL (quando aplicável).

Se você ainda não está convencido, aqui está uma explicação passo a passo que escrevi, O Guia do Mochileiro para prevenção de SQL Injection, onde expliquei detalhadamente todos estes assuntos e até compilei uma secção inteiramente dedicada às más práticas e à sua divulgação.

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

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

E usando DOP em vez de mysql_, mysqli_, e pgsql_ funções, você torna seu aplicativo um pouco mais abstraído do banco de dados, na rara ocorrência de que você precise trocar de provedor de banco de dados.

Usar PDO e consultas preparadas.

($conn é um PDO objeto)

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

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 só uma vez por processo, haveria uma pequena penalidade de desempenho.

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

Minha abordagem:

  • Se você espera que a entrada seja inteira, certifique-se de que seja realmente inteiro.Em uma linguagem de tipo variável como PHP é isto 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 azarar.Se você hexadecimal, escapará perfeitamente de todas as entradas.Em C/C++ existe uma função chamada mysql_hex_string(), em PHP você pode usar bin2hex().

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

  • 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 evitar ataques de injeção de SQL.Observe que você deve preceder os dados com 0x ou use a função MySQL UNHEX em vez de.

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 finalmente quero deixar isso claro.Essas duas abordagens são muito semelhantes, mas são um pouco diferentes em alguns aspectos:

O prefixo ** 0x** só pode ser usado para colunas de dados 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ê terá que substituí-lo totalmente por '', ou você 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 de SQL, onde números inteiros são como strings e escapam apenas com mysql_real_escape_string.Então você pode evitar o uso de aspas.

Por exemplo, se você fizer algo assim:

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

um ataque pode injetar você muito facilmente.Considere o seguinte código injetado retornado do seu script:

SELECIONE...ONDE id = -1 união todos selecionam table_name de information_schema.tables

e agora é só extrair a estrutura da tabela:

SELECIONE...WHERE id = -1 união todos selecionam column_name de information_schema.column onde table_name = 0x61727469636c65

E então basta selecionar os dados desejados.Não é legal?

Mas se o codificador de um site injetável o hexadecimal, nenhuma injeção seria possível porque a consulta ficaria assim: SELECT ... WHERE id = UNHEX('2d312075...3635')

Aviso obsoleto:O código de exemplo desta resposta (como o código de exemplo da pergunta) usa PHP MySQL extensão, que foi obsoleta 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 evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

IMPORTANTE

A melhor maneira de evitar SQL Injection é usar Declarações Preparadas em vez de escapar, como a resposta aceita demonstra.

Existem bibliotecas como Aura.Sql e FácilDB que permitem aos desenvolvedores usar instruções preparadas com mais facilidade.Para saber mais sobre por que as declarações preparadas são melhores em parando a injeção de SQL, referir-se esse mysql_real_escape_string() desviar e corrigiu recentemente vulnerabilidades de injeção SQL Unicode no WordPress.

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

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

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

OBSERVAÇÃO: você deve estar conectado ao banco de dados para usar esta função!

//Conecta 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 em MySQL - Prevenção de injeção de SQL.

Aviso obsoleto:O código de exemplo desta resposta (como o código de exemplo da pergunta) usa PHP MySQL extensão, que foi obsoleta 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 evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

Você poderia fazer algo básico como isto:

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

Isso não resolverá todos os problemas, mas é um bom trampolim.Deixei de fora itens óbvios como verificação da existência da variável, formato (números, letras, etc.).

O que quer que você use, certifique-se de verificar se sua entrada ainda não foi distorcida por magic_quotes ou algum outro lixo bem-intencionado e, se necessário, analise-o stripslashes ou qualquer outra coisa para higienizá-lo.

Aviso obsoleto:O código de exemplo desta resposta (como o código de exemplo da pergunta) usa PHP MySQL extensão, que foi obsoleta 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 evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

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

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`");

Em ambos os casos, você não pode usar ' para proteger o encapsulamento.

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

Na minha opinião, a melhor maneira de evitar a injeção de SQL em seu aplicativo PHP (ou qualquer aplicativo da Web, nesse caso) é pensar na arquitetura do seu aplicativo.Se a única maneira de se proteger contra a injeção de SQL for 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, você está fazendo isso 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.

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

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

  1. A maioria dos bancos de dados (incluindo MySQL) permitem que o acesso do usuário seja restrito à execução de procedimentos armazenados.O controle de acesso de segurança refinado é útil para evitar ataques de escalada de privilégios.Isso evita que aplicativos comprometidos executem 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 compreenderem a estrutura subjacente do banco de dados e projetarem ataques adequados.
  3. Eles aceitam apenas parâmetros, portanto as vantagens das consultas parametrizadas estão aí.Claro - IMO, você ainda precisa limpar 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 forem construídos para aceitar código dinâmico como parâmetros, muitas das vantagens serão negadas.

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

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

Manual PHP para injeção SQL

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

e alguns outros como Prevenindo injeção de SQL com MySQL e PHP

Agora, por que você precisa impedir sua consulta de injeção de SQL?

Eu gostaria que você soubesse: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='admin@emali.com' 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 pode fazer qualquer coisa que o administrador/email pode fazer.Veja, é muito perigoso se a injeção de SQL não for evitada.

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

  1. Pense em aprender DOP (PHP Data Objects) – é uma camada de acesso a banco de dados que fornece um método uniforme de acesso a vários bancos de dados.
  2. Pense em aprender 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 tipos de variáveis ​​em PHP aqui.Se você estiver usando bibliotecas como PDO ou MySQLi, use sempre DOP::citação() e mysqli_real_escape_string().

Exemplos de bibliotecas:

---- DOP

----- Sem espaços reservados - 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();

P.S.:

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 de segurança, os dois são seguros enquanto o desenvolvedor os use da maneira que deveriam ser usados

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

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

Se possível, converta os tipos dos seus parâmetros.Mas funciona apenas 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 . "')");

Se você quiser aproveitar as vantagens dos mecanismos de cache, como Redis ou Memcached, talvez o DALMP possa ser uma escolha.Ele usa puro MySQLi.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 construir 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.

Para aqueles que não têm certeza de como usar o DOP (vindo do mysql_ funções), fiz um wrapper DOP muito, muito simples esse é um único arquivo.Ele existe para mostrar como é fácil fazer todas as coisas comuns que os aplicativos precisam fazer.Funciona com PostgreSQL, MySQL e SQLite.

Basicamente, leia enquanto você lê o manual para ver como colocar as funções PDO em uso na vida real para simplificar o armazenamento e a recuperação de valores no formato você querer.

Eu quero uma única coluna

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

Quero resultados de array(key => value) (ou seja,para fazer uma caixa de seleção)

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

Quero um resultado de linha única

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

Eu quero uma série de resultados

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

Usando esta função PHP mysql_escape_string() você pode obter uma boa prevenção de forma 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

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

Não use MySQL, esta extensão está obsoleta, use MySQLi ou DOP.

MySQLi

Para escapar manualmente de caracteres especiais em uma string, você pode usar o mysqli_real_escape_string função.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 onde os 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, você 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 de variáveis ​​para a função mysqli_stmt_bind_param.

E o uso de mysqli_real_escape_string serve para, como o nome diz, escapar de caracteres especiais em uma string, portanto não tornará os números inteiros seguros.O objetivo desta função é evitar a quebra de strings em instruções SQL e os danos ao banco de dados que isso poderia 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

A alternativa simples para este problema poderia 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 UI 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 às consultas especificadas.Remova a permissão de exclusão e assim os dados nunca serão excluídos da consulta disparada da página PHP.A segunda coisa a fazer é liberar os privilégios para que o MySQL atualize as permissões e atualize.

FLUSH PRIVILEGES; 

mais informações sobre rubor.

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

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

Aprender mais sobre CONCEDER.

Aviso de segurança:Esta resposta não está de acordo com as práticas recomendadas de segurança. O escape é inadequado para evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

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

Eu uso três maneiras diferentes de evitar que meu aplicativo da web fique vulnerável à injeção de SQL.

  1. Uso de mysql_real_escape_string(), que é uma função predefinida em PHP, e este código adiciona barras invertidas aos 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 forma mais avançada é usar DOPs.

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 ​​dentro de 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";

Esse pergunta tem algumas boas respostas sobre isso.

Sugiro que usar DOP seja a melhor opção.

Editar:

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

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");

Em relação a muitas respostas úteis, espero acrescentar alguns valores a este tópico.A injeção de SQL é um ataque que pode ser feito por meio de entradas do usuário (entradas preenchidas pelo usuário e usadas nas consultas). Os padrões de injeção de SQL têm sintaxe de consulta correta, embora possamos chamá-lo:consultas ruins por motivos ruins, presumimos que pode haver uma pessoa má tentando obter informações secretas (contornando o controle de acesso) que afetam os três princípios de segurança (Confidencialidade, Integridade, Disponibilidade).

Agora, nosso objetivo é evitar ameaças à segurança, como ataques de injeção de SQL, a pergunta (Como evitar ataques de injeção de SQL usando PHP), ser mais realista, filtragem de dados ou limpeza de dados de entrada é o caso ao usar dados de entrada do usuário dentro de tais 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 instruçã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 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 inseguros em dados seguros)Considere isso DOP e MySQLi não está disponível, como você pode proteger seu aplicativo?Você me força a usá-los?E quanto a outras linguagens além do PHP?Prefiro fornecer ideias gerais, pois podem ser usadas para fronteiras mais amplas e não apenas para idiomas específicos.

  1. Usuário SQL (limitando privilégios do usuário):as operações SQL mais comuns são (SELECT, UPDATE, INSERT), então, por que conceder o privilégio UPDATE a um usuário que não precisa dele?Por exemplo login e páginas de pesquisa estão usando apenas SELECT, então, 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.

ver Princípio do menor privilégio

  1. Filtragem de dados:antes de construir qualquer consulta, a entrada do usuário 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 de dados.um campo que é um número entre (x e y) deve ser validado exatamente usando a regra exata, para um campo que é uma string (texto):padrão é o caso, por exemplo, nome de usuário deve conter apenas alguns caracteres, digamos [a-zA-Z0-9_-.] o comprimento varia entre (x e n), onde x e n (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 a instrução preparada (consulta parametrizada) e os procedimentos armazenados, as desvantagens aqui são que essas formas requerem habilidades avançadas que não existem para a maioria dos usuários, a ideia básica aqui é distinguir entre a consulta SQL e os dados que é usado internamente, ambas as abordagens podem ser usadas mesmo com dados inseguros, porque os dados de entrada do usuário aqui não acrescentam nada à consulta original, como (qualquer ou x=x).Para mais informações, por favor leia Folha de dicas de prevenção de injeção SQL OWASP.

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

Por fim, vamos considerar que o usuário envia este texto abaixo em vez de inserir 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 instrução preparada e procedimentos armazenados, mas para garantir a segurança, seu uso começa 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, root, uma vez detectadas essas palavras, você pode evitar a entrada.

ATUALIZAÇÃO1:

Um usuário comentou que esse post é inútil, OK!Aqui está o que OWASP.ORG oferecido:

Defesas primárias:

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

Defesas adicionais:

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

Como você deve saber, reivindicar um artigo deve ser apoiado por argumentos válidos, pelo menos uma referência!Caso contrário, é considerado um ataque e uma reclamação incorreta!

Atualização2:

Do manual do PHP, PHP:Declarações Preparadas - Manual:

Escape e injeção de SQL

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

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

Atualização3:

Criei casos de teste para saber como o PDO e o MySQLi enviam a consulta para o servidor MySQL ao usar a 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));

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

Registro 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 escapa dos dados, nada mais.

Como também mencionado na declaração acima The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly, portanto, isso prova que a validação de dados como intval() é uma boa ideia para valores inteiros antes de enviar qualquer consulta, além disso, evitar dados maliciosos do usuário antes de enviar a consulta é abordagem correta e válida.

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

Referências:

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

Uma maneira simples seria usar um framework PHP como Code Igniter ou Laravel que possuem recursos integrados como filtragem e gravação ativa para que você não precise se preocupar com essas nuances.

** 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 de SQL não dependem apenas da capacidade de injetar X=Y.**

Se os invasores estiverem tentando invadir o formulário por meio de PHP $_GET variável ou com a string de consulta do URL, você poderá capturá-los se não forem 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 invasor.Talvez também seja usado por muitos aplicativos de hackers.

Mas você deve ter cuidado para não reescrever uma consulta segura do seu site.O código acima está lhe dando uma dica para reescrever ou redirecionar (depende de você) essa string de consulta dinâmica específica de hacking em uma página que armazenará os dados do invasor endereço de IP, ou MESMO SEUS COOKIES, histórico, navegador ou qualquer outra informação sensível, para que você possa lidar com eles mais tarde, banindo sua conta ou entrando em contato com as autoridades.

Existem tantas respostas para PHP e MySQL, mas aqui está o código para PHP e Oracle para evitar a injeção de SQL, bem como 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);

Uma boa ideia é usar um 'mapeador objeto-relacional' como Idioma:

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

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

Aviso obsoleto:O código de exemplo desta resposta (como o código de exemplo da pergunta) usa PHP MySQL extensão, que foi obsoleta 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 evitar a injeção de SQL, usar declarações preparadas em vez de.Use a estratégia descrita abaixo por sua própria conta e risco.(Também, mysql_real_escape_string() foi removido no PHP 7.)

Usando DOP e MYSQLi é uma boa prática para evitar injeções de SQL, mas se você realmente deseja trabalhar com funções e consultas 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 for uma string, número, caractere ou array, existem muitas funções integradas para detectar isso.Além disso, seria melhor usar essas funções para verificar os dados de entrada.

é_string

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

é_numérico

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

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

Escrevi esta 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 executar instruções em um String.Format C#-ish de uma linha como:

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 tabelas e nomes de colunas, isso falhará, pois coloca todas as strings entre aspas, o que é uma sintaxe inválida.

ATUALIZAÇÃO DE SEGURANÇA:O anterior str_replace versão permitia injeções adicionando {#} tokens aos dados do usuário.Esse preg_replace_callback version não causa problemas se a substituição contiver esses tokens.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top