Database
 sql >> Base de Dados >  >> RDS >> Database

Como proteger um aplicativo JDBC contra injeção de SQL

Visão geral


Em um Relational Database Management System (RDBMS), existe uma linguagem específica – chamada SQL (Structured Query language) – que é usada para se comunicar com o banco de dados. As instruções de consulta escritas em SQL são usadas para manipular o conteúdo e a estrutura do banco de dados. Uma instrução SQL específica que cria e modifica a estrutura do banco de dados é chamada de instrução DDL (Data Definition Language) e as instruções que manipulam o conteúdo do banco de dados são chamadas de instrução DML (Data Manipulation Language). O mecanismo associado ao pacote RDBMS analisa e interpreta a instrução SQL e retorna o resultado de acordo. Este é o processo típico de comunicação com RDBMS—dispare uma instrução SQL e receba de volta o resultado, isso é tudo. O sistema não julga a intenção de qualquer afirmação que se adeque à sintaxe e estrutura semântica da linguagem. Isso também significa que não há processos de autenticação ou validação para verificar quem disparou a instrução e o privilégio de obter a saída. Um invasor pode simplesmente disparar uma instrução SQL com intenção maliciosa e obter informações que não deveria obter. Por exemplo, um invasor pode executar uma instrução SQL com uma carga maliciosa com a consulta de aparência inofensiva para controlar o servidor de banco de dados de um aplicativo da Web.

Como funciona


Um invasor pode aproveitar essa vulnerabilidade e usá-la em seu próprio benefício. Por exemplo, pode-se ignorar o mecanismo de autenticação e autorização de um aplicativo e recuperar o chamado conteúdo seguro de todo o banco de dados. Uma injeção de SQL pode ser usada para criar, atualizar e excluir registros do banco de dados. Pode-se, portanto, formular uma consulta limitada à própria imaginação com SQL.

Normalmente, um aplicativo frequentemente dispara consultas SQL no banco de dados para diversos fins, seja para buscar determinados registros, criar relatórios, autenticar usuários, transações CRUD e assim por diante. O invasor simplesmente precisa encontrar uma consulta de entrada SQL em algum formulário de entrada do aplicativo. A consulta preparada pelo formulário pode ser usada para entrelaçar o conteúdo malicioso de modo que, quando o aplicativo disparar a consulta, ele também carregue a carga útil injetada.

Uma das situações ideais é quando um aplicativo solicita ao usuário uma entrada como nome de usuário ou id de usuário. O aplicativo abriu um ponto vulnerável lá. A instrução SQL pode ser executada sem saber. Um invasor se beneficia injetando uma carga útil que será usada como parte da consulta SQL e processada pelo banco de dados. Por exemplo, o pseudocódigo do lado do servidor para uma operação POST para um formulário de login pode ser:
uname = getRequestString("username");
pass = getRequestString("passwd");

stmtSQL = "SELECT * FROM users WHERE
   user_name = '" + uname + "' AND passwd = '" + pass + "'";

database.execute(stmtSQL);

O código anterior é vulnerável ao ataque de injeção de SQL porque a entrada fornecida à instrução SQL por meio das variáveis ​​'uname' e 'pass' pode ser manipulada de uma maneira que alteraria a semântica da instrução.

Por exemplo, podemos modificar a consulta para ser executada no servidor de banco de dados, como no MySQL.
stmtSQL = "SELECT * FROM users WHERE
   user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";

Isso resulta na modificação da instrução SQL original em um grau que permite ignorar a autenticação. Esta é uma vulnerabilidade séria e deve ser evitada dentro do código.

Defesa contra um ataque de injeção de SQL


Uma das maneiras de reduzir a chance de ataque de injeção de SQL é garantir que as strings de texto não filtradas não possam ser anexadas à instrução SQL antes da execução. Por exemplo, podemos usar PreparedStatement para executar as tarefas de banco de dados necessárias. O aspecto interessante de PreparedStatement é que ele envia uma instrução SQL pré-compilada para o banco de dados, em vez de uma string. Isso significa que a consulta e os dados são enviados separadamente para o banco de dados. Isso evita a causa raiz do ataque de injeção de SQL, porque na injeção de SQL, a ideia é misturar código e dados em que os dados são, na verdade, parte do código disfarçado de dados. Em PreparedStatement , existem vários setXYZ() métodos, como setString() . Esses métodos são usados ​​para filtrar caracteres especiais, como uma cotação contida nas instruções SQL.

Por exemplo, podemos executar uma instrução SQL da seguinte maneira.
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;

Em vez de colocar, digamos, eno=10125 como um número de funcionário na entrada, podemos modificar a consulta com a entrada como:
eno = 10125 OR 1=1

Isso altera completamente o resultado retornado pela consulta.

Um exemplo


No código de exemplo a seguir, mostramos como PreparedStatement pode ser usado para executar tarefas de banco de dados.
package org.mano.example;

import java.sql.*;
import java.time.LocalDate;
public class App
{
   static final String JDBC_DRIVER =
      "com.mysql.cj.jdbc.Driver";
   static final String DB_URL =
      "jdbc:mysql://localhost:3306/employees";
   static final String USER = "root";
   static final String PASS = "secret";
   public static void main( String[] args )
   {
      String selectQuery = "SELECT * FROM employees
         WHERE emp_no = ?";
      String insertQuery = "INSERT INTO employees
         VALUES (?,?,?,?,?,?)";
      String deleteQuery = "DELETE FROM employees
         WHERE emp_no = ?";
      Connection connection = null;
      try {
         Class.forName(JDBC_DRIVER);
         connection = DriverManager.getConnection
            (DB_URL, USER, PASS);
      }catch(Exception ex) {
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(insertQuery);){
         pstmt.setInt(1,99);
         pstmt.setDate(2, Date.valueOf
            (LocalDate.of(1975,12,11)));
         pstmt.setString(3,"ABC");
         pstmt.setString(4,"XYZ");
         pstmt.setString(5,"M");
         pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1)));
         pstmt.executeUpdate();
         System.out.println("Record inserted successfully.");
      }catch(SQLException ex){
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(selectQuery);){
         pstmt.setInt(1,99);
         ResultSet rs = pstmt.executeQuery();
         while(rs.next()){
            System.out.println(rs.getString(3)+
               " "+rs.getString(4));
         }
      }catch(Exception ex){
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(deleteQuery);){
         pstmt.setInt(1,99);
         pstmt.executeUpdate();
         System.out.println("Record deleted
            successfully.");
      }catch(SQLException ex){
         ex.printStackTrace();
      }
      try{
         connection.close();
      }catch(Exception ex){
         ex.printStackTrace();
      }
   }
}

Um vislumbre da PreparedStatement


Esses trabalhos também podem ser realizados com uma Instrução JDBC interface, mas o problema é que pode ser bastante inseguro às vezes, especialmente quando uma instrução SQL dinâmica é executada para consultar o banco de dados onde os valores de entrada do usuário são concatenados com as consultas SQL. Esta pode ser uma situação perigosa, como vimos. Na maioria das circunstâncias normais, Declaração é bastante inofensivo, mas PreparedStatement parece ser a melhor opção entre os dois. Impede que strings maliciosas sejam concatenadas devido à sua abordagem diferente no envio da instrução para o banco de dados. Declaração Preparada usa substituição variável em vez de concatenação. Colocar um ponto de interrogação (?) na consulta SQL significa que uma variável substituta tomará seu lugar e fornecerá o valor quando a consulta for executada. A posição da variável de substituição toma seu lugar de acordo com a posição do índice do parâmetro atribuído no setXYZ() métodos.

Essa técnica impede o ataque de injeção de SQL.

Além disso, PreparedStatement implementa AutoCloseable. Isso permite que ele escreva dentro do contexto de um try-with-resources bloco e fecha automaticamente quando sai do escopo.

Conclusão


Um ataque de injeção de SQL pode ser evitado apenas escrevendo o código com responsabilidade. Na verdade, em qualquer solução de software, a segurança é violada principalmente devido a más práticas de codificação. Aqui, descrevemos o que evitar e como PreparedStatement pode nos ajudar a escrever código seguro. Para uma ideia completa sobre injeção de SQL, consulte os materiais apropriados; a Internet está cheia deles e, para PreparedStatement , consulte a documentação da API Java para obter uma explicação mais detalhada.