Eficiência e segurança com SQL parametrizado

Márcio d'Ávila, 29 de dezembro de 2003. Revisão 7, 1º de setembro de 2008.
Categoria: Banco de Dados: SQL

O uso de comandos SQL, na maioria das linguagens de programação e gerenciadores de bancos de dados que suportam esta linguagem de manipulação de dados, pode ser parametrizado com variáveis de ligação (bind variables). Este recurso que, para um programador desavisado e inexperiente, pode parecer uma burocracia desnecessária, na verdade é um mecanismo muito importante para trazer segurança e eficiência ao uso de SQL em programas. Veja porque e como.

Conceitos

Toda instrução SQL, para ser processada por um servidor de banco de dados, envolve pelo menos duas fases; no caso da consulta (SELECT), há uma terceira fase adicional. Estas três fases são:

Prepare
Preparação da instrução SQL; validação de sintaxe e semântica (parsing) e montagem do plano de execução (determinação da forma como será feito o acesso a dados necessário).
Execute
Execução efetiva da instrução SQL, dentro do banco de dados. No caso de SELECT, também implica em abrir um cursor para iniciar a recuperação das linhas retornadas.
Fetch
Recuperação (leitura) de cada linha retornada, apenas quando se trata de uma consulta.

Os servidores de banco de dados em geral mantêm um cache das instruções SQL já preparadas recentemente, para sua reutilização. Vejamos agora um instrução SQL típica:

SELECT * FROM tabela WHERE coluna = valor

Um valor é uma expressão que pode envolver constantes, outras colunas, ou ainda variáveis de ligação. A variável de ligação (bind variable) funciona como um parâmetro da instrução SQL, sendo um elo de ligação entre o servidor de banco de dados e o ambiente de programação ou interface de acesso que o utiliza, para a passagem de valores dinâmicos a cada execução do SQL.

Variáveis de ligação de entrada podem ocorrer em valores na cláusula WHERE de um SELECT, UPDATE ou DELETE, em valores na cláusula SET do UPDATE, na cláusula VALUES do INSERT ou ainda em parâmetros e valores em um bloco de código procedural. Já variáveis de ligação de saída são o meio de se obter os valores resultantes na cláusula de retorno de um SELECT ou como variáveis de saída de um bloco procedural.

As variáveis de ligação são definidas na fase de preparação de uma instrução SQL. Antes de cada execução desse SQL, deve ser feita a efetiva ligação dos parâmetros, atribuindo-lhes valores. Os bancos de dados possuem diferentes sintaxes para definir variáveis de ligação. Variáveis bind no Oracle são nomes precedidos por dois-pontos ( :nome ). No DB2 e no Interbase, elas são representadas por pontos-de-interrogação ( ? ) e referenciadas por números posicionais 1, 2, 3 etc. No MS SQL Server e no Sybase, são nomes precedidos por um símbolo "arroba" ( @nome ).

Os principais ambientes de programação provêm uma camada de acesso a banco de dados de alto nível que, muitas vezes, possibilita uma sintaxe unificada para especificação de variáveis de ligação em instruções SQL, independente do servidor de banco de dados utilizado. É o caso do JDBC da linguagem Java, onde as variáveis de ligação podem sempre ser definidas em uma instrução SQL com ?.

Eficiência

Uma aplicação com acesso a banco de dados executa determinados SQLs com valores de entrada variáveis a cada execução, tipicamente determinados pela entrada fornecida pelo usuário. Assim, imagine duas consultas executadas, decorrentes de duas escolhas feitas pelo usuário:

SELECT coluna1, coluna2 FROM tabela WHERE chave = 1;
SELECT coluna1, coluna2 FROM tabela WHERE chave = 2;

Para o banco de dados, são duas consultas distintas, embora apenas ligeiramente diferentes, variando somente o valor fornecido. Por serem instruções SQL distintas, antes de executar cada uma delas o banco de dados realiza a fase de preparação, mesmo que ambas levem a planos de execução idênticos. Se por outro lado fosse construído um SQL com variável de ligação:

SELECT coluna1, coluna2 FROM tabela WHERE chave = ?

Este comando é preparado pelo banco de dados, mantido em cache e, antes da execução, deve ser fornecido um valor para a variável de ligação. Para executar o comando com outro valor, basta fornecer o novo valor desejado e executar novamente, reaproveitando a preparação já feita.

Para comandos de banco de dados que são executados várias vezes, o ganho de eficiência com SQL preparado e variáveis de ligação não se trata só de maior velocidade na execução da aplicação, mas também da utilização mais racional e eficiente dos recursos no servidor de banco de dados.

Resumo: Variáveis de ligação (bind) permitem que uma instrução SQL seja preparada uma única vez pelo banco de dados e executada inúmeras vezes, mesmo com valores diferentes para estas variáveis. Esta economia da fase de preparação a cada execução representa um ganho de eficiência (tempo e recursos) na aplicação e no servidor de banco de dados.

Segurança

Suponha que você utilize uma consulta SQL para determinar se a identificação fornecida para um determinado usuário (dada por um par login/senha) está correta, coincidente com o login e senha de um usuário cadastrado. Se este par de identificação está armazenado em uma tabela USUARIO na base de dados, nas colunas LOGIN e SENHA, é comum ser construída a seguinte consulta, que visa localizar com a cláusula WHERE uma linha onde as colunas de LOGIN e SENHA coincidam com os valores fornecidos:

SELECT 1 FROM usuario WHERE login = '...x...' AND senha = '...y...'

Assumindo que o LOGIN seja uma chave única na tabela, esta consulta deve retornar exatamente uma linha caso o login e a senha fornecidos coincidam com os de um usuário, ou nenhuma se um valor ou ambos não forem coincidentes.

Para permitir que os valores de login e senha possam ser fornecidos dinamicamente através de variáveis, poder-se-ia pensar em simplesmente montar o texto da consulta com concatenação de string, combinando as partes fixas do SQL com as variáveis texto. Em linguagens como Java e C#, onde a concatenação de texto pode ser feita com o operador +, a sintaxe seria equivalente a:

"SELECT 1 FROM usuario "
+ "WHERE login = '" + v_login + "' AND senha = '" + v_senha + "'"

Aí está um grande problema: o texto final da consulta SQL depende inteiramente do conteúdo das variáveis. O valor de cada variável concatenada precisará ser devidamente validado e tratado previamente pelo programador, sob pena de o texto final concatenado eventualmente ser um SQL inválido ou adulterado.

O caso mais comum é a presença de um caractere de aspa-simples ou apóstrofo ( ' ) no conteúdo de uma variável concatenada no SQL. Aspas-simples são usadas para delimitar strings de texto em comandos SQL. Se uma única aspa-simples ocorrer em uma das variáveis e este caractere não for tratado antes da concatenação para formar o SQL, ele será interpretado incorretamente como um delimitador de string e o resultado será um SQL inválido. Suponha que no valor da senha seja digitado: abc'def. O texto concatenado resultante seria:

SELECT 1 FROM usuario
WHERE login = 'nome' AND senha = 'abc'def'

A aspa-simples após abc seria interpretada como terminador da string de senha, e o def' a seguir não seria corretamente reconhecido, gerando um erro de sintaxe SQL se este comando fosse submetido para execução no banco de dados.

A situação fica ainda mais perigosa se o usuário que fornece estes valores tentar se aproveitar dessa vulnerabilidade de forma maliciosa. Com conhecimento de SQL, algumas tentativas e um bocado de maldade por parte de um usuário mal-intencionado, pode-se fornecer nos parâmetros trechos de sintaxe SQL tal que a concatenação desses parâmetros nas lacunas do texto SQL pré-programado resulte em um comando SQL válido, mas com comportamento e resultado adulterados em relação ao objetivo original do programador.

Este tipo de manipulação maliciosa de campos ou parâmetros, fornecendo trechos de sintaxe SQL que serão inseridos em comando(s) para o banco de dados, é uma conhecida forma de ataque, denominada injeção de SQL. Os resultados adversos dependem da situação, mas podem levar a acesso indevido sem autenticação (validação de usuário) legítima, obtenção de informações restritas e confidenciais, sobrecarga do servidor, ou qualquer outro mal-funcionamento do sistema devido ao comportamento adulterado da aplicação. O uso direto de valores fornecidos pelo usuário para compor o texto de um comando SQL representa portanto uma grave vulnerabilidade de segurança, com ameaças diretas à autenticação, à confidencialidade e à disponibilidade de todo o sistema: dados, aplicação e servidores envolvidos.

Para estes problemas de validação do conteúdo, algumas medidas de prevenção poderiam ser tomadas pelo programador:

Estas medidas não representam, por si só, boas soluções, consistem em tarefa trabalhosa para o programador e são passíveis de erros, pois:

Vistos todos estes problemas, usar SQL preparado para fornecer valores dinâmicos (parâmetros) nas instruções de banco de dados também é uma solução para validação e segurança. Valores atribuídos através de variáveis de ligação dispensam tratamento de caracteres especiais em strings e eliminam a possibilidade de injeção de SQL. Além disso, a atribuição envolve validação do tipo de dados do valor fornecido, também útil para consistência de números e datas.

Resumo: Uso de variáveis de ligação para valores de entrada em SQL dispensa tratamento de caracteres especiais em strings (como a aspa-simples), facilita a validação de tipo de dados dos valores fornecidos e evita a adulteração maliciosa do SQL, tornando o comando SQL mais seguro e robusto.

Além disso, outras medidas complementares são muito importantes e também devem ser usadas sempre pelo desenvolvedor:

Aproveitando o assunto sobre segurança, é interessante lembrar que também deve haver preocupação análoga do programador web com o tratamento de conteúdo que é inserido em HTML gerado dinamicamente e retornado na saída. Para todo valor fornecido pelo usuário ou lido de fontes de dados variáveis (bases de dados, arquivos, fluxos etc.) e que é inserido no texto-fonte HTML ou XML de resposta, deve haver tratamento de caracteres com significado especial nestes formatos. Em especial, pelo menos os símbolos < , > , & e " devem ser substituídos pelas entidades &lt; , &gt; , &amp; e &quot; respectivamente. Caso contrário, o HTML resultante pode ser inválido ou adulterado, inclusive de forma maliciosa, o que é conhecido como injeção de HTML. Mas como se trata de uma vulnerabilidade na saída gerada por uma aplicação web, a parametrização de SQL para os valores de entrada em nada ajuda neste caso. Para mais informações sobre entidades HTML, veja a Seção 5.3 da Especificação HTML 4.01 pelo W3C, e a Referência das Entidades de Caractere em HTML 4.0 por Alan Wood.

Exemplos

Em linguagem Java, a criação e execução de um comando SQL preparado, com variáveis de ligação para os parâmetros, seria similar ao trecho de código seguinte:

// Cria e prepara um comando SQL com parâmetros (indicados por '?')
// Nota: conn é uma variável do tipo Connection já existente e aberta
String sqlString = "SELECT 1 FROM usuario WHERE login = ? AND senha = ?";
PreparedStatement stmt = conn.prepareStatement(sqlString);

// Associa valores aos parâmetros SQL (bind)
// 1 e 2 especificam a posição (ordem) de cada parâmetro
stmt.setString(1, v_login);
stmt.setString(2, v_senha);

// Executa o comando SQL, com os parâmetros fornecidos
ResultSet result = stmt.executeQuery();
// Segue processando o resultado e encerrando o comando SQL...

A seguir um exemplo similar em C# com OLE-DB. O exemplo seria análogo para ADO.NET com acesso nativo para o banco de dados Microsoft SQL Server, substituindo as classes OleDb... pelas equivalentes Sql... de ADO.NET (SqlConnection, SqlCommand etc.).

// Cria e prepara um comando SQL com parâmetros (indicados por '?')
// Nota: conn é uma variável do tipo OleDbConnection já existente e aberta
string sqlString = "SELECT 1 FROM usuario WHERE login = ? AND senha = ?";
OleDbCommand stmt = new OleDbCommand(sqlString, conn);

// Associa valores aos parâmetros SQL (bind)
// 20 = tamanho da coluna correspondente
stmt.Parameters.Add("login", OleDbType.VarChar, 20).Value = v_login;
stmt.Parameters.Add("senha", OleDbType.VarChar, 20).Value = v_senha;

// Executa o comando SQL, com os parâmetros fornecidos
OleDbDataReader result = stmt.ExecuteReader();
// Segue processando o resultado e encerrando o comando SQL...

E por fim o mesmo exemplo em duas variantes para PHP 5. A primeira utilizando a extensão MySQL Melhorada (mysqli), acessando um servidor MySQL 4.1 ou posterior.

/* Cria e prepara um comando SQL com parâmetros (indicados por '?') */
/* Nota: $conn aponta para uma conexão mysqli já existente e aberta */
$sqlString = 'SELECT 1 FROM usuario WHERE login = ? AND senha = ?';
$stmt = $conn->prepare($sqlString);

/* Associa valores aos parâmetros SQL (bind) */
/* 'ss' indica os tipos dos parâmetros ([s]tring, [s]tring) */
$stmt->bind_param('ss', $v_login, $v_senha);

/* Executa o comando SQL, com os parâmetros fornecidos */
$stmt->execute();
$stmt->bind_result($result);
/* Segue processando o resultado e encerrando o comando SQL... */

A segunda variante utiliza PHP Data Objects (PDO), uma camada de abstração de banco de dados orienta a objetos inclusa no PHP 5.1 (também disponível como extensão PECL para PHP 5.0).

<?php
/* Cria e prepara um comando SQL com parâmetros (indicados por '?') */
/* Nota: $dbh aponta para uma conexão (DB handle) PDO já existente e aberta */
$sqlString = 'SELECT 1 FROM usuario WHERE login = ? AND senha = ?';
$stmt = $dbh->prepare($sqlString);

/* Associa valores aos parâmetros SQL (bind) */
/* 1 e 2 especificam a posição (ordem) de cada parâmetro */
/* 20 = tamanho da coluna correspondente */
$stmt->bindParam(1, $v_login, PDO::PARAM_STR, 20);
$stmt->bindParam(2, $v_senha, PDO::PARAM_STR, 20);

/* Executa o comando SQL, com os parâmetros fornecidos */
$stmt->execute();
/* Segue processando o resultado e encerrando o comando SQL... */
?>

Outra camada de abstração com suporte a SQL parametrizado disponível para PHP5 é o Creole, que tem uma interface de programação orientada a objetos inspirada na API Java JDBC. Assim como PDO, Creole abstrai as APIs nativas do PHP específicas de cada banco de dados, para criar um código portável e independente.

Regra geral

Mesmo quem não tiver familiaridade com todas as linguagens de programação usadas nos exemplos anteriores poderá perceber a grande similaridade entre seus mecanismos, fora meras diferenças de sintaxe de uma linguagem para outra. As operações comuns em todos os casos são:

  1. a criação de um comando SQL Preparado parametrizado;
  2. a atribuição de valores aos parâmetros (bind);
  3. a efetiva execução do comando SQL.

Conclusão

SQL preparado e variáveis de ligação trazem ganho de eficiência às aplicações com banco de dados, evitando o trabalho de preparação repetitiva de instruções SQL executadas múltiplas vezes. Além disso, variáveis de ligação facilitam a validação de tipo de dados dos valores de entrada fornecidos dinamicamente e evitam os riscos de vulnerabilidade de segurança e integridade existentes quando se contrói uma instrução SQL por concatenação de strings. Assim, este recurso traz também robustez e segurança à execução de SQL nas aplicações.

Portanto, há grande importância e vantagens no uso de SQL preparado e variáveis de ligação (bind) nas aplicações interagindo com bancos de dados, especialmente quando envolvem valores dinâmicos e parâmetros fornecidos pelo usuário, de forma que este recurso deve ser utilizado sempre, tratando-se de boa prática de programação.

Referências

Java - JDBC
Microsoft ADO, ADO.NET & OLE-DB
PHP 5 - PDO, MySQLi, Creole e outros
Perl - DBI
Oracle

Firefox - A web de volta
Creative Commons License

© 2003-2020, Márcio d'Ávila, mhavila.com.br, direitos reservados. O texto e código-fonte apresentados podem ser referenciados, distribuídos e utilizados, desde que expressamente citada esta fonte e o crédito do(s) autor(es). A informação aqui apresentada, apesar de todo o esforço para garantir sua precisão e correção, é oferecida "como está", sem quaisquer garantias explícitas ou implícitas decorrentes de sua utilização ou suas conseqüências diretas e indiretas.