postgres postgresql sql injection security

SQL Injection

OWASP

O OWASP (Open Web Application Security Project) é uma fundação com o objetivo de melhorar a segurança de sistemas, em especial aplicações web, através do uso de softwares abertos, ferramentas, recursos, treinamentos e conscientização.

Ele mantém o OWASP Top Ten, que é um documento destinado a desenvolvedores e que relata os dez maiores riscos de segurança para as aplicações web. No topo dessa lista está a injeção de dados.

Uma injeção é quando dados são enxertados em comandos que serão executados. E quando eles causam uma execução inesperada dos comandos, isso pode trazer consequências desastrosas para a confiabilidade, integridade e segurança dos sistemas, como demonstrado pela famosa tirinha XKCD "Exploits of a mom", em que uma injeção maliciosa é responsável pela perda dos registros de alunos de uma escola. Injeções podem afetar tudo, desde protocolos de rede, comandos SQL, arquivos XML, arquivos multimídia, serviços de autenticação como LDAP, cabeçalhos SMTP e HTTP, comandos do sistema operacional, consultas NoSQL e diversos outros.

SQL Injection

A injeção mais popular e a que mais nos interessa agora é a injeção de SQL. Ela acontece quando um comando SQL é construído concatenando textos, especialmente quando isso inclui dados de origem insegura, causando o vazamento de dados ou a execução de outros comandos inesperados.

Por exemplo, se o endpoint de API http://example.com/app/tabelaView?id=123 tiver a seguinte consulta no backend:

String query = "SELECT * FROM tabela WHERE ID='" + request.getParameter("id") + "'";

Que gera o plano de execução similar a:

injetado-OK

Então essa API é suscetível a requisições maliciosas como http://example.com/app/tabelaView?id=' or '1'='1 que, pela concatenação acima, gera o comando abaixo:

SELECT * FROM tabela WHERE ID='' or '1'='1'";

O plano de execução é similar a, que retornará a tabela completa:

injetado-NAO-OK

Mitigações

Existe uma longa lista de ações de mitigação que são usadas para evitar injeções de SQL. Mas é de vital importância compreender quando usar e quando não usar cada uma delas, já que algumas falham em proteger contra injeções, outras aumentam a complexidade do seu código, algumas introduzem bugs tão perigosos quanto as injeções que tentam evitar e ainda outras causam uma falsa sensação de segurança ao mover a responsabilidade para outro componente (normalmente menos confiável). Em resumo, é importante dissolver vários dos mitos que cercam as injeções de SQL. Então vamos ver aqui apenas algumas das mitigações mais comuns.

Quoting e Escaping

Quoting é a ação de adicionar aspas simples, duplas ou outros limitadores de texto em torno do dado que está sendo concatenado, na tentativa de que ele seja interpretado corretamente pelo SGBD. Como visto no exemplo da API, mesmo com aspas simples em torno do campo injetado é possível contornar esse mecanismo quando o dado contém aspas. Portanto, essa estratégia não é uma mitigação válida contra injeção de SQL.

Escaping é a ação de adicionar caracteres especiais para que as aspas do dado não sejam interpretadas como fim do dado, por exemplo tornando ' em \' e \ em \\. Com isso, a consulta anterior não mais retornaria o conteúdo completo da tabela, mas sim um erro de tipo de dados ou uma resposta vazia:

SELECT * FROM accounts WHERE custID='\' or \'1\'=\'1'";

Então aparentemente o problema foi solucionado através do uso simultâneo de quoting e escaping. Contudo, essa solução não é ideal, por diversas razões.

Um erro em qualquer função de quoting ou escaping invalida completamente a estratégia. E erros dessa natureza não são incomuns, com novos bugs sendo descobertos diariamente e atualmente mais de 1350 bugs de quoting listados na base de dados de detalhes de CVE https://www.cvedetails.com/google-search-results.php?q=quoting&sa=Search e 7300 bugs de escaping na mesma base de dados https://www.cvedetails.com/google-search-results.php?q=quoting&sa=Search. Muitos desses bugs são em drivers, bibliotecas, ORMs e frameworks populares, especialmente provenientes de plataformas comunitárias como PyPI, RubyGems e módulos de npm.

Além disso, a consulta fica inchada com os dados enxertados e, consequentemente, o tempo da etapa de parsing dela aumenta. E adicionar mais rodadas de quoting e escaping não diminui os riscos, mas intensifica esses efeitos colaterais.

Então é importante perceber que quoting e escaping nunca serão a solução ideal, então devem ser vistos mais como plano B. Felizmente existem soluções mais seguras, confiáveis e leves disponíveis.

Parametrização

Normalmente quando pensamos em uma aplicação enviando consultas para execução no banco de dados, imaginamos algo extremamente simples e antiquado, em que a consulta inteira é um texto único. E essa visão simples é o que nos faz injetar os parâmetros da consulta (123) no corpo do comando (SELECT). Por exemplo:

╔═══════════╗       ┌───────────────────────────┐  !!!  ╔════════════════╗
║ Aplicação ║->--->-│SELECT ... WHERE id = '123'│->--->-║ Banco de dados ║
╚═══════════╝       └───────────────────────────┘       ╚════════════════╝

Mas a realidade é muito mais complexa. Um fator é o contexto da sessão (como o estado da transação, configurações de sessão, objetos temporários...) que afeta tudo que é executado nela. E outro, tão importante quanto, é a estrutura completa da consulta, que pode ser enviada com muito mais informações que são usadas para transmitir parâmetros separados do corpo da consulta, assim como seus tipos de dados e nulidade. O exemplo a seguir mostra algumas dessas informações (em uma representação parecida com json por familiaridade, mas a comunicação costuma ser feita por protocolos de rede dedicados a esse propósito):

                    ┌─────────────────────────────────┐
╔═══════════╗->--->-│{"q": "SELECT ... WHERE id = $1",│->--->-╔════════════════╗
║ Aplicação ║       │─────────────────────────────────│       ║ Banco de dados ║
╚═══════════╝->--->-│"params": [ { valor: "123",      │->--->-╚════════════════╝
                    │               tipo: "integer",  │
                    │               null: false } ] } │
                    └─────────────────────────────────┘

Dessa forma, mesmo que seja usado um parâmetro com valor malicioso (por exemplo, ' or '1' = '1), ele não corre o risco de ser interpretado como parte do comando, já que a) não foi injetado no corpo do comando; b) o tipo de dados e nulidade podem ser informados e usados como validação extra. Isso é chamado de parametrização de consultas e é a forma mais confiável de evitar injeções de SQL. Em alguns SGBDs, consultas parametrizadas podem ser até mais rápidas que consultas de texto simples por economizar parte do tempo de compilação que antes era desperdiçado com o parse dos parâmetros injetados. Contudo, existem alguns pontos muito importantes que precisamos garantir, mesmo parametrizando as consultas.

Infelizmente muitos softwares não têm esse conceito de consultas propriamente parametrizadas. Mas vários desses têm o conceito de consultas preparadas, que podem ser usadas também para parametrizar as consultas. Exemplos clássicos são PHP e MySQL/MariaDB, que são a razão pela qual uma grande parte das recomendações na internet mencionam "prepared statements". Mas um prepared statement é a solução para um problema distinto (alto tempo da etapa de compilação e otimização de consultas recorrentes), que traz suas próprias desvantagens (maior uso de memória no servidor), então não deveria ser usado sempre. Outros softwares, como o PostgreSQL, têm todas as quatro combinações de consultas parametrizada+não-preparada, parametrizada+preparada, não-parametrizada+preparada, e não-parametrizada+não-preparada.

Ainda outros não têm a possibilidade de consultas não parametrizadas, então toda consulta é parametrizada, por exemplo o MongoDB. Mas ainda é importante garantir que eles nunca interpretem os parâmetros, caso contrário isso abre oportunidade para outros tipos de injeção, como o $where que permite injeção de javascript.

IMPORTANTE: Consultas parametrizadas só são efetivas contra injeções quando os parâmetros nunca são injetados posteriormente nem interpretados no destino. Se algum componente intermediário fizer a injeção, tornando a consulta em um comando de texto único, então as garantias são perdidas. Por exemplo, um ORM pode aceitar consultas parametrizadas, mas ao enviar elas para o banco de dados, o driver faz a injeção arriscada silenciosamente:

                    ┌─────────────────────────────────────┐  !!!  ┌──────────────────────┐  !!!  ╔════════════════╗
╔═══════════╗->--->-│{"query": "SELECT ... WHERE id = $1",│->--->-│ SELECT ... WHERE ... │->--->-║ Banco de dados ║
║ Aplicação ║       │─────────────────────────────────────│    /  │ id = '' or '1' = '1' │       ╚════════════════╝
╚═══════════╝->--->-│"params": [ { valor: "' or '1' = '1",│->-/   └──────────────────────┘
                    │               tipo: "integer",      │
                    │               null: false } ] }     │
                    └─────────────────────────────────────┘

Isso também significa que quanto mais componentes entre o desenvolvedor e o banco de dados, maior o risco de injeção de SQL, já que qualquer um pode fazer a injeção e fazê-la incorretamente. Alguns frameworks, bibliotecas e ORMs fazem essa injeção silenciosa apesar de apresentar APIs parametrizadas para o desenvolvedor (como algumas implementações de API-DB de python e php); alguns drivers de comunicação com bancos de dados também o fazem (como o psycopg2 ainda hoje e alguns drivers JDBC); e até os próprios bancos de dados podem fazer internamente a injeção de parâmetros antes da etapa de parse da consulta.

               >>>    Framework     >>>       ORM        >>>     driver     >>>   protocolo    >>>
                    ┌────────────┐       ┌────────────┐  !!!  ┌──────────┐  !!!  ┌──────────┐  !!!  ╔════════════════╗
╔═══════════╗->--->-│{"q": "...",│->--->-│{"q": "...",│->--->-│SELECT ...│->--->-│SELECT ...│->--->-║ Banco de dados ║
║ Aplicação ║       │────────────│       │────────────│    /  └──────────┘       └──────────┘       ╚════════════════╝
╚═══════════╝->--->-│"p": [...] }│->--->-│"p": [...] }│->-/
                    └────────────┘       └────────────┘

Em resumo, injeções de SQL não podem ser mitigadas apenas pelo uso de uma camada adicional de software - na verdade, camadas adicionais aumentam o risco. Também é impossível garantir que a aplicação esteja imune, já que o erro em qualquer ponto dessa esteira de comunicação será propagado até o banco de dados de forma quase que indetectável.

Conclusão

Além de consultas parametrizadas, muitas outras estratégias também devem ser usadas, como whitelist de valores, análise de código da aplicação, análise de logs do servidor de banco de dados, auditoria, uso de valores com tipos de dados, entre outros. Concatenação de texto nunca deveria ser usada com valores, mas sim deixada apenas para casos em que for estritamente necessária, como para adicionar cláusulas a mais na estrutura da consulta.

Por fim, injeções devem ser tratadas com muita atenção durante o desenvolvimento do software, que deve passar por rigorosas validações de segurança em todos os componentes. Injeções são mais um espectro que nunca deixará de assombrar o mundo da tecnologia.

Grande parte dos ORMs e frameworks prometem impedir SQL injections, mas não poderiam cumprir, já que não têm controle de toda a corrente. Então um elo fraco em qualquer ponto antes ou depois do componente compromete a cadeia inteira. Portanto, é importante perceber que o modelo mental que devemos usar para SQL injections é o de corrente, com cada camada contendo suas fraquezas e podendo quebrar independentemente das outras. É um grande erro usar o modelo de queijo suíço quando tratamos de SQL injection.