Riscos de segurança
Os bancos de dados geralmente contêm dados confidenciais e permitem operações perigosas. O Nette Database oferece vários recursos de segurança. No entanto, é fundamental entender a diferença entre APIs seguras e inseguras.
Injeção de SQL
A injeção de SQL é o risco de segurança mais grave quando se trabalha com bancos de dados. Ela ocorre quando uma entrada de usuário não verificada se torna parte de uma consulta SQL. Um invasor pode injetar seus próprios comandos SQL, obtendo ou modificando dados no banco de dados.
// CÓDIGO INSEGURO - vulnerável à injeção de SQL
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// O invasor pode inserir algo como: ' OR '1'='1
// A consulta resultante será:
// SELECT * FROM users WHERE name = '' OR '1'='1'
// Isso retorna todos os usuários!
O mesmo se aplica ao Database Explorer:
// CÓDIGO INSEGURO
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Consultas parametrizadas seguras
A maneira segura de inserir valores em consultas SQL é por meio de consultas parametrizadas. O Nette Database oferece várias maneiras de usá-las.
Marcadores de interrogação de espaço reservado
O método mais simples é usar pontos de interrogação de espaço reservado:
// Consultas parametrizadas seguras
$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']);
// Condição de segurança no Explorer
$table->where('name = ?', $_GET['name']);
O mesmo se aplica a todos os outros métodos do Database Explorer que permitem a inserção de expressões com pontos de interrogação e parâmetros de espaço reservado.
Os valores devem ser do tipo escalar (string
, int
, float
,
bool
) ou null
. Se, por exemplo, $_GET['name']
for uma matriz, o Nette Database incluirá
todos os seus elementos na consulta SQL, o que pode ser indesejável.
Matrizes de valores
Nas cláusulas INSERT
, UPDATE
ou WHERE
, podemos usar matrizes de valores:
// INSERT seguro
$database->query('INSERT INTO users', [
'name' => $_GET['name'],
'email' => $_GET['email'],
]);
// UPDATE seguro
$database->query('UPDATE users SET', [
'name' => $_GET['name'],
'email' => $_GET['email'],
], 'WHERE id = ?', $_GET['id']);
O Nette Database escapa automaticamente de todos os valores passados por meio de consultas parametrizadas. No entanto, é preciso garantir o tipo de dados correto dos parâmetros.
As chaves de matriz não são uma API segura
Embora os valores em matrizes sejam seguros, o mesmo não se pode dizer das chaves:
// CÓDIGO INSEGURO - as chaves podem conter injeção de SQL
$database->query('INSERT INTO users', $_GET);
$database->query('SELECT * FROM users WHERE', $_GET);
$table->where($_GET);
Para os comandos INSERT
e UPDATE
, essa é uma falha de segurança crítica – um invasor pode
inserir ou modificar qualquer coluna no banco de dados. Por exemplo, ele poderia definir is_admin = 1
ou inserir
dados arbitrários em colunas confidenciais.
Nas condições do WHERE
, isso é ainda mais perigoso porque permite a enumeração do SQL, uma técnica
para recuperar gradualmente informações sobre o banco de dados. Um invasor poderia tentar explorar os salários dos
funcionários injetando em $_GET
dessa forma:
$_GET = ['salary >', 100000]; // começa a determinar as faixas salariais
O principal problema, entretanto, é que as condições do WHERE
suportam expressões SQL nas chaves:
// Uso legítimo de operadores em chaves
$table->where([
'age > ?' => 18,
'ROUND(score, ?) > ?' => [2, 75.5],
]);
// INSEGURO: o invasor pode injetar seu próprio SQL
$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1];
$table->where($_GET); // permite que o invasor obtenha salários de administrador
Isso é mais uma vez injeção de SQL.
Colunas de lista branca
Se quiser permitir que os usuários escolham colunas, use sempre uma lista de permissões:
// Processamento seguro - somente colunas permitidas
$allowedColumns = ['name', 'email', 'active'];
$values = array_intersect_key($_GET, array_flip($allowedColumns));
$database->query('INSERT INTO users', $values);
Identificadores dinâmicos
Para nomes dinâmicos de tabelas e colunas, use o espaço reservado ?name
:
// Uso seguro de identificadores confiáveis
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// UNSAFE - nunca use a entrada do usuário
$database->query('SELECT ?name FROM users', $_GET['column']);
O símbolo ?name
deve ser usado somente para valores confiáveis definidos no código do aplicativo. Para valores
fornecidos pelo usuário, use uma lista branca novamente.