Por que o mecanismo de prevenção de injeção SQL evoluiu na direção do uso de consultas parametrizadas?

59

Do jeito que eu vejo, os ataques de injeção de SQL podem ser evitados por:

  1. Filtrar, filtrar, codificar entrada cuidadosamente (antes da inserção no SQL)
  2. Usando instruções preparadas / consultas parametrizadas
Suponho que existam prós e contras para cada um, mas por que o segundo passo decolou e se tornou mais ou menos a maneira de prevenir ataques de injeção? É apenas mais seguro e menos propenso a erros ou existem outros fatores?

Pelo que entendi, se # 1 for usado corretamente e todas as advertências forem atendidas, pode ser tão eficaz quanto o # 2.

Higienização, filtragem e codificação

Houve alguma confusão da minha parte entre o que saneamento , filtragem , e codificação significava. Eu direi que, para os meus propósitos, todos os itens acima podem ser considerados para a opção 1. Neste caso, eu entendo que sanear e filtrar têm o potencial de modificar ou descartar dados de entrada, enquanto codificam preserva os dados como estão, , mas os codifica adequadamente para evitar ataques de injeção. Acredito que os dados que escapam podem ser considerados como uma maneira de codificá-los.

Consultas parametrizadas versus biblioteca de codificação

Existem respostas onde os conceitos de parameterized queries e encoding libraries são tratados de maneira intercambiável. Corrija-me se estiver errado, mas tenho a impressão de que eles são diferentes.

Meu entendimento é que encoding libraries , não importa o quão bons eles são sempre tem o potencial para modificar o SQL "Programa", porque eles estão fazendo mudanças no próprio SQL, antes de serem enviados para o RDBMS.

Parameterized queries , por outro lado, envia o programa SQL para o RDBMS, que então otimiza a consulta, define o plano de execução da consulta, seleciona os índices que serão usados, etc. e, em seguida, conecta os dados, como a última etapa dentro do próprio RDBMS.

Biblioteca de codificação

  data -> (encoding library)
                  |
                  v
SQL -> (SQL + encoded data) -> RDBMS (execution plan defined) -> execute statement

Consulta parametrizada

                                               data
                                                 |
                                                 v
SQL -> RDBMS (query execution plan defined) -> data -> execute statement

Significado histórico

Algumas respostas mencionam que, historicamente, as consultas parametrizadas (PQ) foram criadas por motivos de desempenho e, antes dos ataques de injeção, os problemas de codificação direcionados se tornaram populares. Em algum momento, tornou-se evidente que o QP também era bastante eficaz contra ataques de injeção. Para manter o espírito da minha pergunta, por que o PQ continuou sendo o método de escolha e por que ele se desenvolveu acima da maioria dos outros métodos quando se trata de impedir ataques de injeção de SQL?

    
por Dennis 12.09.2016 / 16:04
fonte

14 respostas

146

O problema é que # 1 requer que você analise e interprete a totalidade da variante do SQL em que está trabalhando, para saber se está fazendo algo que não deveria. E mantenha esse código atualizado enquanto atualiza seu banco de dados. Everywhere você aceita informações para suas consultas. E não estrague tudo.

Então, sim, esse tipo de coisa interromperia os ataques de injeção de SQL, mas é absurdamente mais caro implementar.

    
por 12.09.2016 / 16:08
fonte
79

Porque a opção 1 não é uma solução. Triagem e filtragem significa rejeitar ou remover entradas inválidas. Mas qualquer entrada pode ser válida. Por exemplo, o apóstrofo é um caractere válido no nome "O'Malley". Ele só precisa ser codificado corretamente antes de ser usado no SQL, que é o que instruções preparadas fazem.

Depois de adicionar a nota, parece que você está basicamente perguntando por que usar uma função de biblioteca padrão em vez de escrever seu próprio código funcionalmente similar do zero? Você deve sempre preferir soluções de biblioteca padrão para escrever seu próprio código. É menos trabalho e mais sustentável. Este é o caso de qualquer qualquer funcionalidade, mas especialmente para algo que é sensível à segurança não faz absolutamente nenhum sentido para reinventar a roda em seu próprio país.

    
por 12.09.2016 / 18:07
fonte
60

Se você está tentando fazer o processamento de strings, você não está realmente gerando uma consulta SQL. Você está gerando uma string que pode produzir uma consulta SQL. Há um nível de indireção que abre um lote de espaço para erros e bugs. É um tanto surpreendente, já que na maioria dos contextos estamos felizes em interagir com algo programaticamente. Por exemplo, se tivermos alguma estrutura de lista e quisermos adicionar um item, geralmente não o fazemos:

List<Integer> list = /* a list of 1, 2, 3 */
String strList = list.toString();   /* to get "[1, 2, 3]" */
strList = /* manipulate strList to become "[1, 2, 5, 3]" */
list = parseList(strList);

Se alguém sugerir isso, você diria que é um pouco ridículo e que deve ser feito:

List<Integer> list = /* ... */;
list.add(5, position=2);

Isso interage com a estrutura de dados em seu nível conceitual. Ele não apresenta nenhuma dependência de como essa estrutura pode ser impressa ou analisada. Essas são decisões completamente ortogonais.

Sua primeira abordagem é como o primeiro exemplo (apenas um pouco pior): você está supondo que pode construir programaticamente a string que será analisada corretamente como a consulta desejada. Isso depende do analisador e de um monte de lógica de processamento de string.

A segunda abordagem de usar consultas preparadas é muito mais parecida com a segunda amostra. Quando você usa uma consulta preparada, basicamente analisa uma pseudo-consulta que é legal, mas tem alguns marcadores de posição nela e, em seguida, usa uma API para substituir corretamente alguns valores nela. Você não envolve mais o processo de análise e não precisa se preocupar com o processamento de strings.

Em geral, é muito mais fácil e menos propenso a erros interagir com as coisas em seu nível conceitual. Uma consulta não é uma string, uma consulta é o que você obtém quando você analisa uma string, ou constrói uma programaticamente (ou qualquer outro método que permita criar uma).

Há uma boa analogia aqui entre as macros de estilo C que fazem a substituição de texto simples e as macros no estilo Lisp que fazem a geração de código arbitrário. Com macros no estilo C, você pode substituir o texto no código-fonte, e isso significa que você tem a capacidade de introduzir erros sintáticos ou comportamento enganoso. Com macros Lisp, você está gerando código na forma que o compilador o processa (ou seja, você está retornando as estruturas de dados reais que o compilador processa, não o texto que o leitor deve processar antes que o compilador possa obtê-lo) . Com uma macro Lisp, você não pode gerar algo que seria um erro de análise. Por exemplo, você não pode gerar (let ((a b) a .

Mesmo com as macros Lisp, você ainda pode gerar códigos ruins, porque você não precisa necessariamente estar ciente da estrutura que deveria estar lá. Por exemplo, em Lisp, (let ((ab)) a) significa "estabelecer uma nova ligação léxica da variável a com o valor da variável b e, em seguida, retornar o valor de a" e < strong> (let (ab) a) significa "estabelecer novas ligações lexicais das variáveis aebe inicializá-las para nil e, em seguida, retornar o valor de a." Esses são ambos sintaticamente corretos, mas significam coisas diferentes. Para evitar esse problema, você pode usar mais funções semânticas e fazer algo como:

Variable a = new Variable("a");
Variable b = new Variable("b");
Let let = new Let();
let.getBindings().add(new LetBinding(a,b));
let.setBody(a);
return let;

Com algo assim, é impossível retornar algo que é sintaticamente inválido, e é muito mais difícil retornar algo que não seja o que você queria.

    
por 13.09.2016 / 00:06
fonte
21

Isso ajuda que a opção 2 seja geralmente considerada uma prática recomendada, pois o banco de dados pode armazenar em cache a versão não-parametrizada da consulta. Consultas parametrizadas são anteriores à questão da injeção de SQL por vários anos (creio eu), acontece que você pode matar dois coelhos com uma cajadada só.

    
por 12.09.2016 / 19:29
fonte
20

Simplesmente disse: Eles não fizeram. Sua declaração:

Why did SQL Injection prevention mechanism evolve into the direction of using Parameterized Queries?

é fundamentalmente falho. As consultas parametrizadas já existem há muito mais tempo do que o SQL Injection pelo menos amplamente conhecido. Eles foram geralmente desenvolvidos como uma maneira de evitar a concentração de strings na funcionalidade usual de "formulário para pesquisa" que os aplicativos LOB (Linha de Negócios) possuem. Muitos - MUITOS - anos mais tarde, alguém encontrou um problema de segurança com a manipulação da string.

Eu me lembro de fazer SQL há 25 anos (quando a internet não era muito usada - estava apenas começando) e me lembro de fazer SQL vs. IBM DB5 IIRC versão 5 - e isso já tinha parametrizado consultas.

    
por 14.09.2016 / 13:44
fonte
13

Além de todas as outras boas respostas:

A razão pela qual # 2 é melhor é porque separa seus dados do seu código. No primeiro lugar, os seus dados fazem parte do seu código e é daí que vêm todas as coisas ruins. Com o número 1, você obtém sua consulta e precisa executar etapas adicionais para garantir que sua consulta entenda seus dados como dados, enquanto que na # 2 você recebe seu código e seu código e seus dados são dados.

    
por 13.09.2016 / 08:56
fonte
11

As consultas parametrizadas, além de fornecer defesa de injeção de SQL, geralmente têm um benefício adicional de serem compiladas apenas uma vez e, em seguida, executadas várias vezes com parâmetros diferentes.

Do ponto de vista do banco de dados SQL select * from employees where last_name = 'Smith' e select * from employees where last_name = 'Fisher' são distintamente diferentes e, portanto, exigem análise, compilação e otimização separadas. Eles também ocuparão slots separados na área de memória dedicada ao armazenamento de instruções compiladas. Em um sistema altamente carregado, com um grande número de consultas semelhantes que têm diferentes parâmetros, a computação e a sobrecarga de memória podem ser substanciais.

Posteriormente, o uso de consultas parametrizadas geralmente fornece grandes vantagens de desempenho.

    
por 12.09.2016 / 19:36
fonte
5

Espere, mas por quê?

A opção 1 significa que você precisa escrever rotinas de limpeza para cada tipo de entrada, enquanto a opção 2 é menos sujeita a erros e menos código para escrever / testar / manter.

Quase certamente "cuidando de todas as advertências" pode ser mais complexo do que você pensa, e sua linguagem (por exemplo, Java PreparedStatement) tem mais problemas do que você pensa.

As instruções preparadas ou as consultas parametrizadas são pré-compiladas no servidor de banco de dados, portanto, quando os parâmetros são definidos, nenhuma concatenação SQL é feita porque a consulta não é mais uma string SQL. Uma vantagem adicional é que o RDBMS armazena em cache a consulta e as chamadas subseqüentes são consideradas como sendo o mesmo SQL, mesmo quando os valores dos parâmetros variam, enquanto com SQL concatenado toda vez que a consulta é executada com valores diferentes, a consulta é diferente e o RDBMS precisa analisá-la , crie o plano de execução novamente, etc.

    
por 12.09.2016 / 16:17
fonte
1

Vamos imaginar como seria uma abordagem ideal de "higienizar, filtrar e codificar".

O saneamento e a filtragem podem fazer sentido no contexto de um aplicativo específico, mas no final ambos se resumem em dizer "você não pode colocar esses dados no banco de dados". Para o seu aplicativo, isso pode ser uma boa ideia, mas não é algo que você possa recomendar como uma solução geral, pois haverá aplicativos que precisam ser capazes de armazenar caracteres arbitrários no banco de dados.

Isso deixa a codificação. Você poderia começar por ter uma função que codifica strings adicionando caracteres de escape, para que você possa substituí-los em si mesmo. Como bancos de dados diferentes precisam de caracteres diferentes de escape (em alguns bancos de dados, \' e '' são sequências de escape válidas para ' , mas não em outros), essa função precisa ser fornecida pelo fornecedor do banco de dados.

Mas nem todas as variáveis são strings. Às vezes você precisa substituir em um número inteiro ou uma data. Estes são representados de forma diferente para strings, então você precisa de diferentes métodos de codificação (mais uma vez, eles precisariam ser específicos para o fornecedor do banco de dados), e você precisa substituí-los na consulta de maneiras diferentes.

Então, talvez seja mais fácil se o banco de dados manuseou a substituição também - já sabe quais tipos a consulta espera e como codificar os dados com segurança, e como substituí-los na sua consulta com segurança, para que você não precise se preocupar com isso em seu código.

Neste ponto, acabamos de reinventar as consultas parametrizadas.

E, uma vez parametrizadas as consultas, elas abrem novas oportunidades, como otimizações de desempenho e monitoramento simplificado.

A codificação é difícil de fazer, e a codificação-feita-direita é indistinguível da parametrização.

Se você realmente gosta de interpolação de strings como uma forma de construir consultas, há algumas linguagens (Scala e ES2015 vêm à mente) que têm interpolação de strings plugáveis, então são bibliotecas que permitem escrever consultas parametrizadas que se parecem com a interpolação de strings, mas são seguras da injeção de SQL - Então, na sintaxe do ES2015:

import {sql} from 'cool-sql-library'

let result = sql'select *
    from users
    where user_id = ${user_id}
      and password_hash = ${password_hash}'.execute()

console.log(result)
    
por 14.09.2016 / 18:08
fonte
0

Na opção 1, você está trabalhando com um conjunto de entrada de tamanho = infinito que está tentando mapear para um tamanho de saída muito grande. Na opção 2, você limitou sua entrada ao que você escolher. Em outras palavras:

  1. Filtre e filtre com cuidado [ infinity ] para [ todas as consultas SQL seguras ]
  2. Usando [ cenários pré-considerados limitados ao seu escopo ]

De acordo com outras respostas, parece também haver alguns benefícios de desempenho ao limitar seu alcance longe do infinito e para algo gerenciável.

    
por 12.09.2016 / 22:34
fonte
0

Um modelo mental útil de SQL (especialmente dialetos modernos) é que cada instrução ou consulta SQL é um programa. Em um programa executável binário nativo, os tipos mais perigosos de vulnerabilidades de segurança são os overflows, em que um invasor pode sobrescrever ou modificar o código do programa com instruções diferentes.

Uma vulnerabilidade de injeção de SQL é isomórfica a um estouro de buffer em uma linguagem como C. A história mostrou que os estouro de buffer são extremamente difíceis de evitar - mesmo o código extremamente crítico sujeito a revisão aberta frequentemente continha tais vulnerabilidades.

Um aspecto importante da abordagem moderna para a solução de vulnerabilidades de estouro é o uso de hardware e mecanismos de SO para marcar partes específicas da memória como não executáveis e para marcar outras partes da memória como somente leitura. (Veja o artigo da Wikipedia sobre Proteção de espaço executável , por exemplo.) Dessa forma, mesmo se um invasor puder modificar dados, o O invasor não pode fazer com que os dados injetados sejam tratados como código.

Portanto, se uma vulnerabilidade de injeção SQL é equivalente a um estouro de buffer, qual é o equivalente SQL a um bit NX ou a páginas de memória somente leitura? A resposta é: instruções preparadas , que incluem consultas parametrizadas e mecanismos semelhantes para solicitações que não são de consulta. A instrução preparada é compilada com certas partes marcadas como somente leitura, portanto, um invasor não pode alterar essas partes do programa e outras partes marcadas como dados não executáveis (os parâmetros da instrução preparada), em que o invasor poderia injetar dados, mas que nunca será tratado como código de programa, eliminando assim a maior parte do potencial de abuso.

Certamente, limpar a entrada do usuário é bom, mas para estar realmente seguro, você precisa ser paranóico (ou, equivalentemente, pensar como um invasor). Uma superfície de controle fora do texto do programa é a maneira de fazer isso, e instruções preparadas fornecem essa superfície de controle para SQL. Portanto, não é nenhuma surpresa que as declarações preparadas e, portanto, as consultas parametrizadas, sejam a abordagem recomendada pela maioria dos profissionais de segurança.

    
por 13.09.2016 / 08:23
fonte
0

Eu já escrevi sobre isso aqui: link

Mas, só para simplificar:

O modo como as consultas parametrizadas funcionam é que o sqlQuery é enviado como uma consulta, e o banco de dados sabe exatamente o que essa consulta fará, e somente então ele inserirá o nome de usuário e senhas apenas como valores. Isso significa que eles não podem efetuar a consulta, porque o banco de dados já sabe o que a consulta fará. Portanto, nesse caso, ele procuraria um nome de usuário de "Ninguém OU 1 = 1" - e uma senha em branco, que deve aparecer como falsa.

Esta não é uma solução completa, e a validação de entrada ainda precisará ser feita, já que isso não afetará outros problemas, como ataques XSS, pois você ainda pode colocar o javascript no banco de dados. Então, se isso for lido em uma página, ele será exibido como um javascript normal, dependendo de qualquer validação de saída. Então, realmente a melhor coisa a fazer é ainda usar validação de entrada, mas usando consultas parametrizadas ou procedimentos armazenados para parar qualquer ataque SQL

    
por 16.09.2016 / 10:25
fonte
0

Eu nunca usei SQL. Mas, obviamente, você ouve sobre quais problemas as pessoas têm, e os desenvolvedores de SQL tiveram problemas com essa coisa de "injeção de SQL". Por muito tempo eu não consegui descobrir. E então percebi que as pessoas que criam instruções SQL, instruções de origem SQL textuais reais, concatenando cadeias de caracteres, algumas das quais inseridas por um usuário. E meu primeiro pensamento sobre essa percepção foi choque. Choque total. Eu pensei: como alguém pode ser tão ridiculamente estúpido e criar declarações em qualquer linguagem de programação como essa? Para um desenvolvedor C, ou C ++, ou Java, ou Swift, isso é loucura total.

Dito isso, não é muito difícil escrever uma função C que use uma string C como argumento e produza uma string diferente que se pareça exatamente com uma string literal no código-fonte C que represente a mesma string. Por exemplo, essa função traduziria abc para "abc" e "abc" para "\" abc \ "" e "\" abc \ "" para "\" \\ "abc \\" \ "". (Bem, se isso parece errado para você, isso é html. Foi bem quando eu digitei, mas não quando ele é exibido) E uma vez que a função C é escrita, não é difícil para gerar o código-fonte C onde o texto de um campo de entrada fornecido pelo usuário é transformado em um literal de string C. Isso não é difícil de fazer seguro. Por que os desenvolvedores de SQL não usariam essa abordagem como uma maneira de evitar injeções de SQL está além de mim.

"Saneantes" é uma abordagem totalmente falha. A falha fatal é que torna certas entradas do usuário ilegais. Você acaba com um banco de dados em que um campo de texto genérico não pode conter texto como; Drop Table ou o que você usaria em uma injeção SQL para causar danos. Acho isso inaceitável. Se um banco de dados armazena texto, ele deve ser capaz de armazenar qualquer texto . E a falha prática é que o sanitizante não consegue acertar: - (

Naturalmente, consultas parametrizadas são o que qualquer programador usando uma linguagem compilada estaria esperando. Isso torna a vida muito mais fácil: você tem alguma entrada de strings, e você nunca se incomodou em traduzi-la em uma string SQL, apenas passa-a como um parâmetro, sem chance de qualquer caractere na string causar algum dano.

Então, do ponto de vista de um desenvolvedor usando linguagens compiladas, sanear é algo que nunca me ocorreria. A necessidade de saneantes é insana. Consultas parametrizadas são a solução óbvia para o problema.

(Eu achei a resposta de Josip interessante. Ele basicamente diz que com consultas parametrizadas você pode parar qualquer ataque contra SQL, mas então você pode ter texto em seu banco de dados que é usado para criar uma injeção de JavaScript :-( Bem, nós temos o mesmo problema novamente, e eu não sei se o Javascript tem uma solução para isso.

    
por 15.05.2017 / 23:45
fonte
-2

O principal problema é que os hackers encontraram maneiras de cercar o saneamento, enquanto as consultas parametrizadas eram um procedimento existente que funcionava perfeitamente com os benefícios extras de desempenho e memória.

Algumas pessoas simplificam o problema como "é apenas a aspa e aspas duplas", mas os hackers descobriram maneiras inteligentes de evitar a detecção, como usar codificações diferentes ou fazer uso de funções de banco de dados.

De qualquer forma, você só precisava esquecer uma única string para criar uma violação de dados catastrófica. Os hackers podem automatizar scripts para baixar o banco de dados completo com uma série ou consultas. Se o software for bem conhecido como um conjunto de código aberto ou um conjunto de negócios famoso, você pode simplesmente chamar a tabela de usuários e senhas.

Por outro lado, apenas usando consultas concatenadas era apenas uma questão de aprender a usar e se acostumar com isso.

    
por 14.09.2016 / 17:59
fonte