Na primeira parte deste blog, descrevemos como o ProxySQL pode ser usado para bloquear consultas de entrada consideradas perigosas. Como você viu nesse blog, conseguir isso é muito fácil. Esta não é uma solução completa, no entanto. Você pode precisar projetar uma configuração ainda mais segura - você pode querer bloquear todas as consultas e permitir que apenas algumas selecionadas passem. É possível usar ProxySQL para fazer isso. Vejamos como isso pode ser feito.
Existem duas maneiras de implementar a lista de permissões no ProxySQL. Primeiro, o histórico, seria criar uma regra geral que bloquearia todas as consultas. Deve ser a última regra de consulta na cadeia. Um exemplo abaixo:
Estamos correspondendo a cada string e geramos uma mensagem de erro. Esta é a única regra existente no momento, ela impede que qualquer consulta seja executada.
mysql> USE sbtest;
Database changed
mysql> SELECT * FROM sbtest1 LIMIT 10;
ERROR 1148 (42000): This query is not on the whitelist, you have to create a query rule before you'll be able to execute it.
mysql> SHOW TABLES FROM sbtest;
ERROR 1148 (42000): This query is not on the whitelist, you have to create a query rule before you'll be able to execute it.
mysql> SELECT 1;
ERROR 1148 (42000): This query is not on the whitelist, you have to create a query rule before you'll be able to execute it.
Como você pode ver, não podemos executar nenhuma consulta. Para que nosso aplicativo funcione, teríamos que criar regras de consulta para todas as consultas que desejamos permitir a execução. Isso pode ser feito por consulta, com base no resumo ou padrão. Você também pode permitir o tráfego com base em outros fatores:nome de usuário, host do cliente, esquema. Vamos permitir SELECTs para uma das tabelas:
Agora podemos executar consultas nesta tabela, mas não em qualquer outra:
mysql> SELECT id, k FROM sbtest1 LIMIT 2;
+------+------+
| id | k |
+------+------+
| 7615 | 1942 |
| 3355 | 2310 |
+------+------+
2 rows in set (0.01 sec)
mysql> SELECT id, k FROM sbtest2 LIMIT 2;
ERROR 1148 (42000): This query is not on the whitelist, you have to create a query rule before you'll be able to execute it.
O problema com esta abordagem é que ela não é tratada de forma eficiente no ProxySQL, portanto no ProxySQL 2.0.9 vem com novo mecanismo de firewall que inclui novo algoritmo, focado neste caso de uso específico e como tal mais eficiente. Vamos ver como podemos usá-lo.
Primeiro, temos que instalar o ProxySQL 2.0.9. Você pode baixar pacotes manualmente em https://github.com/sysown/proxysql/releases/tag/v2.0.9 ou pode configurar o repositório ProxySQL.
Uma vez feito isso, podemos começar a examiná-lo e tentar configurá-lo para usar o firewall SQL.
O processo em si é bastante fácil. Antes de tudo, você precisa adicionar um usuário à tabela mysql_firewall_whitelist_users. Ele contém todos os usuários para os quais o firewall deve ser ativado.
mysql> INSERT INTO mysql_firewall_whitelist_users (username, client_address, mode, comment) VALUES ('sbtest', '', 'DETECTING', '');
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL FIREWALL TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
Na consulta acima, adicionamos o usuário ‘sbtest’ à lista de usuários que devem ter o firewall habilitado. É possível dizer que apenas as conexões de um determinado host são testadas em relação às regras do firewall. Você também pode ter três modos:'OFF', quando o firewall não é usado, 'DETECTING', onde as consultas incorretas são registradas, mas não bloqueadas e 'PROTECTING', onde as consultas não permitidas não serão executadas.
Vamos habilitar nosso firewall:
mysql> SET mysql-firewall_whitelist_enabled=1;
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL VARIABLES TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
O firewall ProxySQL baseia-se no resumo das consultas, não permite o uso de expressões regulares. A melhor maneira de coletar dados sobre quais consultas devem ser permitidas é usar a tabela stats.stats_mysql_query_digest, onde você pode coletar consultas e seus resumos. Além disso, o ProxySQL 2.0.9 vem com uma nova tabela:history_mysql_query_digest, que é uma extensão persistente da tabela na memória mencionada anteriormente. Você pode configurar o ProxySQL para armazenar dados em disco de tempos em tempos:
mysql> SET admin-stats_mysql_query_digest_to_disk=30;
Query OK, 1 row affected (0.00 sec)
A cada 30 segundos, os dados sobre as consultas serão armazenados no disco. Vamos ver como acontece. Executaremos algumas consultas e, em seguida, verificaremos seus resumos:
mysql> SELECT schemaname, username, digest, digest_text FROM history_mysql_query_digest;
+------------+----------+--------------------+-----------------------------------+
| schemaname | username | digest | digest_text |
+------------+----------+--------------------+-----------------------------------+
| sbtest | sbtest | 0x76B6029DCBA02DCA | SELECT id, k FROM sbtest1 LIMIT ? |
| sbtest | sbtest | 0x1C46AE529DD5A40E | SELECT ? |
| sbtest | sbtest | 0xB9697893C9DF0E42 | SELECT id, k FROM sbtest2 LIMIT ? |
+------------+----------+--------------------+-----------------------------------+
3 rows in set (0.00 sec)
À medida que configuramos o firewall para o modo 'DETECTING', também veremos entradas no log:
2020-02-14 09:52:12 Query_Processor.cpp:2071:process_mysql_query(): [WARNING] Firewall detected unknown query with digest 0xB9697893C9DF0E42 from user [email protected]
2020-02-14 09:52:17 Query_Processor.cpp:2071:process_mysql_query(): [WARNING] Firewall detected unknown query with digest 0x76B6029DCBA02DCA from user [email protected]
2020-02-14 09:52:20 Query_Processor.cpp:2071:process_mysql_query(): [WARNING] Firewall detected unknown query with digest 0x1C46AE529DD5A40E from user [email protected]
Agora, se quisermos começar a bloquear consultas, devemos atualizar nosso usuário e definir o modo como 'PROTEGER'. Isso bloqueará todo o tráfego, então vamos começar colocando as consultas na lista de permissões acima. Então vamos habilitar o modo 'PROTEGER':
mysql> INSERT INTO mysql_firewall_whitelist_rules (active, username, client_address, schemaname, digest, comment) VALUES (1, 'sbtest', '', 'sbtest', '0x76B6029DCBA02DCA', ''), (1, 'sbtest', '', 'sbtest', '0xB9697893C9DF0E42', ''), (1, 'sbtest', '', 'sbtest', '0x1C46AE529DD5A40E', '');
Query OK, 3 rows affected (0.00 sec)
mysql> UPDATE mysql_firewall_whitelist_users SET mode='PROTECTING' WHERE username='sbtest' AND client_address='';
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL FIREWALL TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
mysql> SAVE MYSQL FIREWALL TO DISK;
Query OK, 0 rows affected (0.08 sec)
É isso. Agora podemos executar consultas na lista de permissões:
mysql> SELECT id, k FROM sbtest1 LIMIT 2;
+------+------+
| id | k |
+------+------+
| 7615 | 1942 |
| 3355 | 2310 |
+------+------+
2 rows in set (0.00 sec)
Mas não podemos executar os que não estão na lista de permissões:
mysql> SELECT id, k FROM sbtest3 LIMIT 2;
ERROR 1148 (42000): Firewall blocked this query
O ProxySQL 2.0.9 vem com outro recurso de segurança interessante. Ele tem libsqlinjection embutido e você pode habilitar a detecção de possíveis injeções de SQL. A detecção é baseada nos algoritmos da libsqlinjection. Esse recurso pode ser ativado executando:
mysql> SET mysql-automatic_detect_sqli=1;
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL VARIABLES TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
Funciona com o firewall da seguinte forma:
- Se o firewall estiver ativado e o usuário estiver no modo PROTEGER, a detecção de injeção de SQL não será usada, pois somente as consultas explicitamente permitidas podem passar.
- Se o firewall estiver ativado e o usuário estiver no modo DETECTING, as consultas da lista de permissões não serão testadas para injeção de SQL, todas as outras serão testadas.
- Se o firewall estiver ativado e o usuário estiver no modo 'OFF', todas as consultas serão consideradas na lista de permissões e nenhuma será testada para injeção de SQL.
- Se o firewall estiver desabilitado, todas as consultas serão testadas para detecção de SQL.
Basicamente, é usado apenas se o firewall estiver desabilitado ou para usuários no modo 'DETECÇÃO'. A detecção de injeção de SQL, infelizmente, vem com muitos falsos positivos. Você pode usar a tabela mysql_firewall_whitelist_sqli_fingerprints para colocar na lista branca as impressões digitais para consultas que foram detectadas incorretamente. Vamos ver como isso funciona. Primeiro, vamos desabilitar o firewall:
mysql> set mysql-firewall_whitelist_enabled=0;
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL VARIABLES TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
Então, vamos executar algumas consultas.
mysql> SELECT id, k FROM sbtest2 LIMIT 2;
ERROR 2013 (HY000): Lost connection to MySQL server during query
De fato, existem falsos positivos. No log encontramos:
2020-02-14 10:11:19 MySQL_Session.cpp:3393:handler(): [ERROR] SQLinjection detected with fingerprint of 'EnknB' from client [email protected] . Query listed below:
SELECT id, k FROM sbtest2 LIMIT 2
Ok, vamos adicionar esta impressão digital à tabela da lista de permissões:
mysql> INSERT INTO mysql_firewall_whitelist_sqli_fingerprints VALUES (1, 'EnknB');
Query OK, 1 row affected (0.00 sec)
mysql> LOAD MYSQL FIREWALL TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
Agora podemos finalmente executar esta consulta:
mysql> SELECT id, k FROM sbtest2 LIMIT 2;
+------+------+
| id | k |
+------+------+
| 84 | 2456 |
| 6006 | 2588 |
+------+------+
2 rows in set (0.01 sec)
Tentamos executar a carga de trabalho do sysbench, o que resultou em mais duas impressões digitais adicionadas à tabela da lista de permissões:
2020-02-14 10:15:55 MySQL_Session.cpp:3393:handler(): [ERROR] SQLinjection detected with fingerprint of 'Enknk' from client [email protected] . Query listed below:
SELECT c FROM sbtest21 WHERE id=49474
2020-02-14 10:16:02 MySQL_Session.cpp:3393:handler(): [ERROR] SQLinjection detected with fingerprint of 'Ef(n)' from client [email protected] . Query listed below:
SELECT SUM(k) FROM sbtest32 WHERE id BETWEEN 50053 AND 50152
Queríamos ver se essa injeção de SQL automatizada pode nos proteger contra nosso bom amigo, Booby Tables.
mysql> CREATE TABLE school.students (id INT, name VARCHAR(40));
Query OK, 0 rows affected (0.07 sec)
mysql> INSERT INTO school.students VALUES (1, 'Robert');DROP TABLE students;--
Query OK, 1 row affected (0.01 sec)
Query OK, 0 rows affected (0.04 sec)
mysql> SHOW TABLES FROM school;
Empty set (0.01 sec)
Infelizmente, não realmente. Por favor, tenha em mente que este recurso é baseado em algoritmos forenses automatizados, está longe de ser perfeito. Pode vir como uma camada adicional de defesa, mas nunca poderá substituir o firewall mantido adequadamente criado por alguém que conhece o aplicativo e suas consultas.
Esperamos que, depois de ler esta curta série de duas partes, você tenha uma melhor compreensão de como proteger seu banco de dados contra injeção de SQL e tentativas maliciosas (ou simplesmente erros de usuário) usando ProxySQL. Se você tiver mais ideias, adoraríamos ouvi-lo nos comentários.