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.
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:
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 ?
.
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.
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:
''
. Essa preocupação porém
se torna fonte freqüente de enganos pelo programador, que pode se esquecer
de tratar algum caractere especial, ou ainda confundir a sintaxe dos
caracteres especiais e do escape de strings entre a linguagem SQL e a
linguagem de programação utilizada na aplicação.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.
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
<
, >
,
&
e "
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.
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. 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.
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:
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.
© 2003-2024, 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.