Riscos de segurança
O banco de dados frequentemente contém dados sensíveis e permite a execução de operações perigosas. Para trabalhar com segurança com Nette Database, é crucial:
- Compreender a diferença entre API segura e insegura
- Usar consultas parametrizadas
- Validar corretamente os dados de entrada
O que é SQL Injection?
SQL injection é o risco de segurança mais grave ao trabalhar com um banco de dados. Ocorre quando uma entrada não tratada do usuário se torna parte de uma consulta SQL. Um invasor pode inserir seus próprios comandos SQL e, assim:
- Obter acesso não autorizado aos dados
- Modificar ou excluir dados no banco de dados
- Contornar a autenticação
// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// O invasor pode inserir, por exemplo, o valor: ' OR '1'='1
// A consulta resultante será: SELECT * FROM users WHERE name = '' OR '1'='1'
// O que retorna todos os usuários
O mesmo se aplica ao Database Explorer:
// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Consultas parametrizadas
A defesa básica contra SQL injection são as consultas parametrizadas. Nette Database oferece várias maneiras de usá-las.
A maneira mais simples é usar placeholders de interrogação:
// ✅ Consulta parametrizada segura
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Condição segura no Explorer
$table->where('name = ?', $name);
Isso se aplica a todos os outros métodos no Database Explorer que permitem inserir expressões com placeholders de interrogação e parâmetros.
Para comandos INSERT, UPDATE ou a cláusula WHERE, podemos passar valores em um array:
// ✅ INSERT seguro
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ INSERT seguro no Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Validação dos valores dos parâmetros
Consultas parametrizadas são o pilar fundamental do trabalho seguro com bancos de dados. No entanto, os valores que inserimos nelas devem passar por vários níveis de verificação:
Verificação de tipo
O mais importante é garantir o tipo de dados correto dos parâmetros – esta é uma condição necessária para o uso seguro do Nette Database. O banco de dados assume que todos os dados de entrada têm o tipo de dados correto correspondente à coluna específica.
Por exemplo, se $name
nos exemplos anteriores fosse inesperadamente um array em vez de uma string, o Nette
Database tentaria inserir todos os seus elementos na consulta SQL, o que levaria a um erro. Portanto, nunca use dados não
validados de $_GET
, $_POST
ou $_COOKIE
diretamente em consultas de banco de dados.
Verificação de formato
No segundo nível, verificamos o formato dos dados – por exemplo, se as strings estão na codificação UTF-8 e seu comprimento corresponde à definição da coluna, ou se os valores numéricos estão dentro do intervalo permitido para o tipo de dados da coluna.
Neste nível de validação, podemos confiar parcialmente no próprio banco de dados – muitos bancos de dados rejeitarão dados inválidos. No entanto, o comportamento pode variar, alguns podem truncar silenciosamente strings longas ou cortar números fora do intervalo.
Verificação de domínio
O terceiro nível representa verificações lógicas específicas da sua aplicação. Por exemplo, verificar se os valores das caixas de seleção correspondem às opções oferecidas, se os números estão no intervalo esperado (por exemplo, idade 0–150 anos) ou se as dependências mútuas entre os valores fazem sentido.
Métodos de validação recomendados
- Use Nette Forms, que garantem automaticamente a validação correta de todas as entradas
- Use Presenters e especifique os tipos de dados para os parâmetros nos
métodos
action*()
erender*()
- Ou implemente sua própria camada de validação usando ferramentas PHP padrão como
filter_var()
Trabalho seguro com colunas
Na seção anterior, mostramos como validar corretamente os valores dos parâmetros. No entanto, ao usar arrays em consultas SQL, devemos prestar a mesma atenção às suas chaves.
// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas
$database->query('INSERT INTO users', $_POST);
Para comandos INSERT e UPDATE, isso é uma falha de segurança crítica – um invasor pode inserir ou alterar qualquer coluna
no banco de dados. Ele poderia, por exemplo, definir is_admin = 1
ou inserir dados arbitrários em colunas sensíveis
(a chamada Mass Assignment Vulnerability).
Nas condições WHERE, é ainda mais perigoso, pois podem conter operadores:
// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// executa a consulta WHERE (`salary` > 100000)
Um invasor pode usar essa abordagem para descobrir sistematicamente os salários dos funcionários. Ele pode começar, por exemplo, com uma consulta por salários acima de 100.000, depois abaixo de 50.000 e, estreitando gradualmente o intervalo, pode revelar os salários aproximados de todos os funcionários. Esse tipo de ataque é chamado de SQL enumeration.
Os métodos where()
e whereOr()
são ainda muito mais flexíveis e suportam expressões SQL, incluindo
operadores e funções, nas chaves e valores. Isso dá ao invasor a possibilidade de realizar SQL injection:
// ❌ CÓDIGO PERIGOSO - o invasor pode inserir seu próprio SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// executa a consulta WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Este ataque encerra a condição original com 0)
, anexa seu próprio SELECT
usando UNION
para obter dados sensíveis da tabela users
e fecha a consulta sintaticamente correta com WHERE (1)
.
Whitelist de colunas
Para trabalhar com segurança com nomes de colunas, precisamos de um mecanismo que garanta que o usuário só possa trabalhar com colunas permitidas e não possa adicionar as suas próprias. Poderíamos tentar detectar e bloquear nomes de colunas perigosos (blacklist), mas essa abordagem não é confiável – um invasor sempre pode encontrar uma nova maneira de escrever um nome de coluna perigoso que não previmos.
Portanto, é muito mais seguro inverter a lógica e definir uma lista explícita de colunas permitidas (whitelist):
// Colunas que o usuário pode editar
$allowedColumns = ['name', 'email', 'active'];
// Removemos todas as colunas não permitidas da entrada
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Agora podemos usar com segurança em consultas, como por exemplo:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificadores dinâmicos
Para nomes dinâmicos de tabelas e colunas, use o placeholder ?name
. Isso garante o escape correto dos
identificadores de acordo com a sintaxe do banco de dados específico (por exemplo, usando crases no MySQL):
// ✅ Uso seguro de identificadores confiáveis
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Resultado no MySQL: SELECT `name` FROM `users`
Importante: use o símbolo ?name
apenas para valores confiáveis definidos no código da aplicação. Para
valores do usuário, use novamente a whitelist. Caso contrário, você se expõe a riscos
de segurança:
// ❌ PERIGOSO - nunca use entrada do usuário
$database->query('SELECT ?name FROM users', $_GET['column']);