injeção de SQL

Categoria do OWASP: MASVS-CODE - Qualidade do código

Visão geral

A injeção de SQL explora aplicativos vulneráveis inserindo o código em instruções SQL para acessar bancos de dados além das interfaces expostas intencionalmente. O ataque pode expor dados particulares, corromper o conteúdo do banco de dados e até mesmo comprometer a infraestrutura do back-end.

O SQL pode ficar vulnerável à injeção por consultas criadas dinamicamente, concatenando a entrada do usuário antes da execução. Direcionada para a Web, dispositivos móveis e qualquer aplicativo de banco de dados SQL, a injeção de SQL geralmente aparece na lista do OWASP de dez principais vulnerabilidades da Web (link em inglês). Invasores usaram essa técnica em várias violações importantes.

Neste exemplo básico, uma entrada sem escape feita por um usuário em uma caixa de número de pedido pode ser inserida na string SQL e interpretada como a seguinte consulta:

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

Esse código geraria um erro de sintaxe de banco de dados em um console da Web, o que mostra que o aplicativo pode estar vulnerável à injeção de SQL. Substituir o número do pedido por 'OR 1=1– significa que a autenticação pode ser feita, porque o banco de dados avalia a instrução como True, já que um é sempre é igual a um.

Da mesma forma, esta consulta retorna todas as linhas de uma tabela:

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

Provedores de conteúdo

Os provedores de conteúdo oferecem um mecanismo de armazenamento estruturado que pode ser limitado a um aplicativo ou exportado para compartilhamento com outros apps. As permissões precisam ser definidas com base no princípio de privilégio mínimo. Um ContentProvider exportado pode ter uma única permissão especificada para leitura e gravação.

Nem todas as injeções de SQL levam à exploração de vulnerabilidades. Alguns provedores de conteúdo já concedem aos leitores acesso completo ao banco de dados SQLite. A capacidade de executar consultas arbitrárias gera pouca vantagem. Os padrões que podem representar um problema de segurança incluem:

  • Vários provedores de conteúdo que compartilham um único arquivo de banco de dados SQLite.
    • Nesse caso, cada tabela pode ser destinada a um provedor de conteúdo exclusivo. Uma injeção de SQL bem-sucedida em um provedor de conteúdo concederia acesso a qualquer outra tabela.
  • Um provedor de conteúdo tem várias permissões para conteúdo dentro do mesmo banco de dados.
    • A injeção de SQL em um único provedor de conteúdo que concede acesso com diferentes níveis de permissão pode levar ao cancelamento local das configurações de segurança ou privacidade.

Impacto

A injeção de SQL pode expor dados sensíveis do usuário ou do aplicativo, superar restrições de autenticação e autorização e deixar os bancos de dados vulneráveis a corrupção ou exclusão. Os impactos podem incluir implicações perigosas e duradouras para usuários com dados pessoais expostos. Os provedores de apps e serviços correm o risco de perder a propriedade intelectual ou a confiança do usuário.

Mitigações

Parâmetros substituíveis

O uso de ? como um parâmetro substituível em cláusulas de seleção e uma matriz separada de argumentos de seleção vincula a entrada do usuário diretamente à consulta em vez de interpretá-la como parte de uma instrução SQL.

Kotlin

// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"

// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")

// Adds values to the selection arguments array.
selectionArgs[0] = userInput

Java

// Constructs a selection clause with a replaceable parameter.
String selectionClause =  "var = ?";

// Sets up an array of arguments.
String[] selectionArgs = {""};

// Adds values to the selection arguments array.
selectionArgs[0] = userInput;

A entrada do usuário é vinculada diretamente à consulta em vez de ser tratada como SQL, impedindo a injeção de código.

Confira um exemplo mais elaborado que mostra a consulta de um app de compras para extrair detalhes da compra com parâmetros substituíveis:

Kotlin

fun validateOrderDetails(email: String, orderNumber: String): Boolean {
    val cursor = db.rawQuery(
        "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
        arrayOf(email, orderNumber)
    )

    val bool = cursor?.moveToFirst() ?: false
    cursor?.close()

    return bool
}

Java

public boolean validateOrderDetails(String email, String orderNumber) {
    boolean bool = false;
    Cursor cursor = db.rawQuery(
      "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?", 
      new String[]{email, orderNumber});
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            bool = true;
        }
        cursor.close();
    }
    return bool;
}

Usar objetos PreparedStatement

A interface PreparedStatement pré-compila instruções SQL como um objeto que pode ser executado de forma eficiente várias vezes. A PreparedStatement usa ? como um marcador de posição para parâmetros, o que tornaria esta tentativa de injeção compilada ineficaz:

WHERE id=295094 OR 1=1;

Nesse caso, a instrução 295094 OR 1=1 é lida como o valor de ID, provavelmente não tendo resultados, enquanto uma consulta bruta interpretaria a instrução OR 1=1 como outra parte da cláusula WHERE. O exemplo abaixo mostra uma consulta parametrizada:

Kotlin

val pstmt: PreparedStatement = con.prepareStatement(
        "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
    setString(1, "Barista")
    setInt(2, 295094)
}

Java

PreparedStatement pstmt = con.prepareStatement(
                                "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")   
pstmt.setInt(2, 295094)

Usar métodos de consulta

Neste exemplo mais longo, a selection e os selectionArgs do método query() são combinados para criar uma cláusula WHERE. Como os argumentos são fornecidos separadamente, eles têm escape antes da combinação, evitando a injeção de SQL.

Kotlin

val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
)

// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")

// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"

val cursor = db.query(
    FeedEntry.TABLE_NAME,  // The table to query
    projection,            // The array of columns to return
                           //   (pass null to get all)
    selection,             // The columns for the WHERE clause
    selectionArgs,         // The values for the WHERE clause
    null,                  // Don't group the rows
    null,                  // Don't filter by row groups
    sortOrder              // The sort order
).use {
    // Perform operations on the query result here.
    it.moveToFirst()
}

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Usar o SQLiteQueryBuilder configurado corretamente

Os desenvolvedores podem proteger ainda mais os aplicativos usando SQLiteQueryBuilder, uma classe que ajuda a criar consultas a serem enviadas para objetos SQLiteDatabase. As configurações recomendadas incluem:

Usar a biblioteca do Room

O pacote android.database.sqlite fornece APIs necessárias para usar bancos de dados no Android. No entanto, essa abordagem exige uma codificação de baixo nível e não inclui a verificação durante a compilação de consultas SQL brutas. À medida que os gráficos de dados mudam, as consultas SQL afetadas precisam ser atualizadas manualmente. Esse é um processo demorado e propenso a erros.

Uma solução de alto nível é usar a Biblioteca Room Persistence como uma camada de abstração para bancos de dados SQLite. Estes são os recursos do Room:

  • Uma classe de banco de dados que serve como o ponto de acesso principal para se conectar aos dados persistentes do app.
  • Entidades de dados que representam as tabelas do banco de dados.
  • Objetos de acesso a dados (DAOs, na sigla em inglês), que fornecem métodos que o app pode usar para consultar, atualizar, inserir e excluir dados.

Os benefícios do Room incluem:

  • Verificação de consultas SQL durante a compilação.
  • Redução de código boilerplate propenso a erros.
  • Migração simplificada de banco de dados.

Práticas recomendadas

A injeção de SQL é um ataque poderoso. Pode ser difícil se proteger totalmente contra ela, especialmente com aplicativos grandes e complexos. Outras considerações de segurança precisam ser feitas para limitar a gravidade de possíveis falhas nas interfaces de dados, incluindo:

  • Use hashes robustos, unidirecionais e com sal para criptografar senhas:
    • Use AES de 256 bits para aplicativos comerciais.
    • Use tamanhos de chave pública de 224 ou 256 bits para elliptic curve cryptography (criptografia de curva elíptica).
  • Limite as permissões.
  • Use formatos de dados bem estruturados e faça a verificação da conformidade dos dados com o formato esperado.
  • Evite armazenar dados pessoais ou sensíveis do usuário sempre que possível, por exemplo, implementando a lógica do aplicativo com hash em vez de transmitir ou armazenar dados.
  • Minimize o uso de APIs e aplicativos de terceiros que acessam dados sensíveis.

Recursos