2 Secure API Development - API Security in Action
2 Secure API Development - API Security in Action
Até agora, falei sobre a segurança da API de forma abstrata, mas neste capítulo,
você se aprofundará e examinará os detalhes básicos do desenvolvimento de uma
API de exemplo. Eu escrevi muitas APIs em minha carreira e agora passo meus
dias revisando a segurança das APIs usadas para operações críticas de segurança
em grandes corporações, bancos e organizações de mídia multinacionais. Embora
as tecnologias e técnicas variem de situação para situação e de ano para ano, os
fundamentos permanecem os mesmos. Neste capítulo, você aprenderá como apli-
car os princípios básicos de desenvolvimento seguro ao desenvolvimento de API,
para que possa criar medidas de segurança mais avançadas sobre uma base
sólida.
Embora este cenário possa não ser muito parecido com qualquer coisa em que
você esteja trabalhando, se você está lendo este livro, é provável que em algum
momento você tenha que pensar sobre a segurança de uma API que você proje-
tou, construiu ou solicitado a manter. Neste capítulo, você criará um exemplo de
API de brinquedo, verá exemplos de ataques contra essa API e aprenderá como
aplicar princípios básicos de desenvolvimento seguro para eliminar esses
ataques.
A implantação geral da API é mostrada na figura 2.1. As duas APIs são expostas
por HTTP e usam JSON para o conteúdo da mensagem, tanto para clientes móveis
quanto para web. As conexões com o banco de dados compartilhado usam SQL
padrão sobre a API JDBC do Java.
A Figura 2.1 Natter expõe duas APIs - uma para usuários normais e outra para
moderadores. Para simplificar, ambos compartilham o mesmo banco de dados. Os
clientes móveis e da web se comunicam com a API usando JSON sobre HTTP, em-
bora as APIs se comuniquem com o banco de dados usando SQL sobre JDBC.
Uma solicitação HTTP POST para o /spaces URI cria um novo espaço social. O
usuário que executa esta operação POST torna-se o proprietário do novo es-
paço. Um identificador exclusivo para o espaço é retornado na resposta.
Os usuários podem adicionar mensagens a um espaço social enviando uma soli-
citação POST para /spaces/ <spaceId>/messages onde <spaceId> está o
identificador exclusivo do espaço.
As mensagens em um espaço podem ser consultadas usando uma solicitação
GET para /spaces/ <spaceId>/messages . Um since=<timestamp> parâ-
metro de consulta pode ser usado para limitar as mensagens retornadas a um
período recente.
Por fim, os detalhes de mensagens individuais podem ser obtidos usando uma
solicitação GET para /spaces/<spaceId>/messages/<messageId> .
A API do moderador contém uma única operação para excluir uma mensagem en-
viando uma solicitação DELETE para o URI da mensagem. Uma coleção do Post-
man para ajudá-lo a usar a API está disponível em
https://www.getpostman.com/collections/ef49c7f5cba0737ecdfd . Para importar a
coleção no Postman, vá para Arquivo, Importar e selecione a guia Link. Em se-
guida, insira o link e clique em Continuar.
Neste capítulo, você implementará apenas a operação para criar um novo espaço
social. As operações de postagem de mensagens em um espaço e leitura de mensa-
gens são deixadas como exercício. O repositório GitHub que acompanha o livro (
https://github.com/NeilMadden/ apisecurityinaction ) contém exemplos de imple-
mentações das operações restantes no capítulo 02-endramo.
2.1.2 Visão geral da implementação
Instruções detalhadas sobre como instalar essas dependências para Mac, Win-
dows e Linux estão no apêndice A. Se você não tiver todos ou algum deles insta-
lado, certifique-se de tê-los prontos antes deProsseguir.
DICA Para uma melhor experiência de aprendizado, é uma boa ideia digitar as
listagens deste livro à mão, para ter certeza de que entendeu cada linha. Mas se
você quiser ir mais rápido, o código-fonte completo de cada capítulo está disponí-
vel no GitHub em https://github.com/NeilMadden/apisecurityinaction . Siga as ins-
truções no arquivo README.md para configurar.
Se esta é a primeira vez que você usa o Maven, pode levar algum tempo para bai-
xar as dependências necessárias. Depois de concluído, você terá a seguinte estru-
tura de projeto, contendo o arquivo de projeto Maven inicial (pom.xml) e uma
App classee AppTest classe de teste de unidade na estrutura de pasta do pacote
Java necessária.
natter-api
├── pom.xml ❶
└── origem
├── principal
│ └── java
│ └── com
│ └── tripulação
│ └── apisecurityinaction
│ └── App.java ❷
└── teste
└── java
└── com
└── tripulação
└── apisecurityinaction
└── AppTest.java ❸
Primeiro, você precisa substituir o arquivo de projeto Maven gerado por um que
liste as dependências que você usará. Localize o arquivo pom.xml e abra-o em seu
editor ou IDE favorito. Selecione todo o conteúdo do arquivo e exclua-o, cole o
conteúdo da listagem 2.1 no editor e salve o novo arquivo. Isso garante que o Ma-
ven esteja configurado para Java 11, configura a classe principal para apontar
para a Main classe(a ser escrito em breve) e configura todas as dependências que
você precisa.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.manning.api-security-in-action</groupId>
<artifactId>natter-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
<propriedades>
<maven.compiler.source>11</maven.compiler.source> ❶
<maven.compiler.target>11</maven.compiler.target> ❶
<exec.mainClass>
com.manning.apisecurityinaction.Main ❷
</exec.mainClass>
</propriedades>
<dependências>
<dependência>
<groupId>com.h2database</groupId> ❸
<artifactId>h2</artifactId> ❸
<version>1.4.197</version> ❸
</dependency> ❸
<dependency> ❸
<groupId>com.sparkjava</groupId> ❸
<artifactId>spark-core</artifactId> ❸
<version>2.9.2</version> ❸
</dependency> ❸
<dependency> ❸
<groupId>org.json</groupId> ❸
<artifactId>json</artifactId> ❸
<version>20200518</version> ❸
</dependency> ❸
<dependency> ❸
<groupId>org.dalesbred</groupId> ❸
< artefatoId>dalesbred</artifactId> ❸
<version>1.3.2</version> ❸
</dependency> ❸
<dependência>
<groupId>org.slf4j</groupId> ❹
<artifactId>slf4j-simple</artifactId> ❹
<version>1.7.30</version> ❹
</dependência>
</dependencies>
</projeto>
Agora você pode excluir os arquivos App.java e AppTest.java, pois estará escre-
vendo novas versões deles enquantovai.
pacote com.manning.apisecurityinaction;
importar java.nio.file.*;
import org.dalesbred.*;
import org.h2.jdbcx.*;
import org.json.*;
Agoraque você tem o banco de dados no lugar, você pode começar a escrever as
APIs REST reais que o utilizam. Você aprofundará os detalhes da implementação à
medida que avançamos no capítulo, aprendendo os princípios de desenvolvi-
mento seguro à medida que avança.
Figura 2.3 Uma operação de API geralmente pode ser separada em três fases: pri-
meiro analisamos a entrada e extraímos as variáveis de interesse, depois executa-
mos a operação real e, finalmente, preparamos alguma saída que indica o status
da operação.
Nesse caso, você usará a biblioteca json.org para analisar o corpo da solicitação
como JSON e extrair o nome e o proprietário do novo espaço. Você então usará
Dalesbred para iniciar uma transação no banco de dados e criar o novo espaço in-
serindo uma nova linha no spaces banco de dadostabela. Por fim, se tudo foi
bem-sucedido, você criará uma resposta 201 Created com algum JSON descre-
vendo o espaço recém-criado. Como é necessário para uma resposta HTTP 201,
você definirá o URI do espaço recém-criado no cabeçalho Location da resposta.
Navegue até o projeto Natter API que você criou e localize a pasta
src/main/java/com/manning/apisecurityinaction. Crie uma nova subpasta cha-
mada “controller” neste local. Em seguida, abra seu editor de texto e crie um novo
arquivo chamado SpaceController.java nesta nova pasta. A estrutura do arquivo
resultante deve ter a seguinte aparência, com os novos itens destacados em
negrito:
natter-api
├── pom.xml
└── origem
├── principal
│ └── java
│ └── com
│ └── tripulação
│ └── apisecurityinaction
│ ├── Main.java
│ └── controlador
│ └── SpaceController.java
└── teste
└── ...
pacote com.manning.apisecurityinaction.controller;
import org.dalesbred.Database;
import org.json.*;
importar faísca.*;
public class SpaceController {
banco de dados de banco de dados final privado;
public SpaceController(banco de dados do banco de dados) {
this.database = banco de dados;
}
public JSONObject createSpace(solicitação de solicitação, resposta de resposta)
lança SQLException {
var json = new JSONObject(request.body()); ❶
var spaceName = json.getString("nome");
var proprietário = json.getString("proprietário");
return database.withTransaction(tx -> { ❷
var spaceId = database.findUniqueLong( ❸
"SELECT PRÓXIMO VALOR PARA space_id_seq;"); ❸
// AVISO: esta próxima linha de código contém um
// vulnerabilidade de segurança!
database.updateUnique(
"INSERT INTO espaços(space_id, nome, proprietário)" +
"VALUES(" + spaceId + ", '" + spaceName +
"', '" + proprietário + "');");
resposta.status(201); ❹
response.header("Localização", "/espaços/" + spaceId); ❹
retornar novo JSONObject()
.put("nome", espaçoNome)
.put("uri", "/espaços/" + spaceId);
});
}
}
Agoraque você criou o controlador, você precisa conectá-lo para que seja cha-
mado quando um usuário fizer uma solicitação HTTP para criar um espaço. Para
fazer isso, você precisará criar uma nova rota do Sparkque descreve como combi-
nar solicitações HTTP recebidas com métodos em nossos objetos controladores.
Na listagem 2.5, você usará importações estáticas para acessar a API do Spark.
Isso não é estritamente necessário, mas é recomendado pelos desenvolvedores do
Spark porque pode tornar o código mais legível. Então você precisa criar uma ins-
tância do seu SpaceController objetoque você criou na última seção, passando
no Dalesbred Database objeto para que ele possa acessar o banco de dados. Você
pode então configurar as rotas Spark para chamar métodos no objeto controlador
em resposta a solicitações HTTP. Por exemplo, a linha de código a seguir organiza
a chamada do método createSpace quando uma solicitação HTTP POST é recebida
para o URI /spaces:
post("/spaces", spaceController::createSpace);
Por fim, como todas as suas respostas de API serão JSON, adicionamos um af-
ter filtro Sparkpara definir o cabeçalho Content-Type na resposta
a application/json em todos os casos, qual é o tipo de conteúdo correto para
JSON. Como veremos mais tarde, é importante definir cabeçalhos de tipo correto
em todas as respostas para garantir que os dados sejam processados conforme
pretendido pelo cliente. Também adicionamos alguns manipuladores de erro para
produzir respostas JSON corretas para erros internos do servidor e erros não en-
contrados (quando um usuário solicita um URI que não possui uma rota definida).
DICA O Spark possui três tipos de filtros (figura 2.4). Os filtros anteriores são exe-
cutados antes que a solicitação seja tratada e são úteis para validação e definição
de padrões. Os filtros posteriores são executados depois que a solicitação foi tra-
tada, mas antes de qualquer manipulador de exceção (se o processamento da soli-
citação gerou uma exceção). Existem também pós-filtros, que são executados após
todos os outros processamentos, incluindo manipuladores de exceção e, portanto,
são úteis para definir os cabeçalhos que você deseja apresentar em todas as
respostas.
Figura 2.4 Os filtros anteriores do Spark são executados antes que a solicitação
seja processada por seu manipulador de solicitações. Se o manipulador for con-
cluído normalmente, o Spark executará qualquer filtro posterior. Se o manipula-
dor lançar uma exceção, o Spark executará o manipulador de exceção correspon-
dente em vez dos filtros posteriores. Por fim, os filtros afterAfter são sempre exe-
cutados após o processamento de cada solicitação.
pacote com.manning.apisecurityinaction;
import com.manning.apisecurityinaction.controller.*;
import org.dalesbred.Database;
import org.h2.jdbcx.JdbcConnectionPool;
import org.json.*;
importar java.nio.file.*;
var espaçoController =
novo SpaceController(banco de dados); ❷
post("/spaces", ❸
spaceController::createSpace); ❸
internalServerError(novo JSONObject()
.put("erro", "erro interno do servidor").toString());
notFound(novo JSONObject()
.put("erro", "não encontrado").toString());
}
❸ Isso lida com solicitações POST para o endpoint /spaces chamando o método crea-
teSpace em seu objeto controlador.
❹ Adicionamos alguns filtros básicos para garantir que toda saída seja sempre tra-
tada como JSON.
2.3.1 Experimentando
Agora que temos uma operação de API escrita, podemos iniciar o servidor e testá-
lo. A maneira mais simples de começar a funcionar é abrindo um terminal na
pasta do projeto e usando o Maven:
Você deve ver a saída de log para indicar que o Spark iniciou um servidor Jetty in-
corporado na porta 4567. Você pode usar curl para chamar sua operação de API,
como no seguinteexemplo:
{"nome":"espaço de teste","uri":"/espaços/1"}
Infelizmente,o código que você acabou de escrever tem uma grave vulnerabili-
dade de segurança, conhecida como ataque de injeção de SQL. Os ataques de inje-
ção são uma das vulnerabilidades mais difundidas e graves em qualquer aplica-
tivo de software. Atualmente, a injeção é a entrada número um no Top 10 da
OWASP (veja a barra lateral).
Os 10 melhores do OWASP
É importante observar que, embora valha a pena aprender sobre cada vulnerabi-
lidade no Top 10, evitar o Top 10 não tornará seu aplicativo seguro por si só. Não
existe uma lista de verificação simples de vulnerabilidades a serem evitadas. Em
vez disso, este livro ensinará os princípios gerais para evitar classes inteiras de
vulnerabilidades.
Um ataque de injeção pode ocorrer em qualquer lugar em que você execute có-
digo dinâmico em resposta à entrada do usuário, como consultas SQL e LDAP, e ao
executar comandos do sistema operacional.
eval() Se você implementar sua API em uma linguagem dinâmica, sua lingua-
gem pode ter uma função integradaavaliar uma string como código e passar uma
entrada de usuário não validada para tal função seria uma coisa muito perigosa
de se fazer, porque pode permitir que o usuário execute código arbitrário com to-
das as permissões de seu aplicativo. Mas há muitos casos em que você está avali-
ando um código que pode não ser tão óbvio quanto chamar uma eval função ex-
plícita, como:
Essa confusão é o que permite que um invasor obtenha o controle. A linha incor-
reta do código é a seguinte, que concatena o nome do espaço fornecido pelo usuá-
rio e o proprietário na INSERT instrução SQL:
database.updateUnique(
"INSERT INTO espaços(space_id, nome, proprietário)" +
"VALUES(" + spaceId + ", '" + spaceName +
"', '" + proprietário + "');");
o spaceId é um valor numérico que é criado pelo seu aplicativo a partir de uma
sequência, portanto é relativamente seguro, mas as outras duas variáveis vêm di-
retamente do usuário. Nesse caso, a entrada vem da carga JSON, mas também
pode vir de parâmetros de consulta na própria URL. Todos os tipos de solicitações
são potencialmente vulneráveis a ataques de injeção, não apenas os métodos
POST que incluem uma carga útil.
No SQL, os valores de string são colocados entre aspas simples e você pode ver
que o código se preocupa em adicioná-los ao redor da entrada do usuário. Mas o
que acontece se a própria entrada do usuário contiver uma aspa simples? Vamos
tentar e ver:
A aspa simples que você incluiu em sua entrada acabou causando um erro de sin-
taxe na expressão SQL. O que o banco de dados vê é a string 'test' , seguida por
alguns caracteres extras (“espaço”) e depois outra aspa simples. Como esta não é
uma sintaxe SQL válida, ele reclama e aborta a transação. Mas e se sua entrada
acabar sendo um SQL válido? Nesse caso, o banco de dados irá executá-lo sem re-
clamar. Vamos tentar executar o seguinte comando:
A operação foi concluída com sucesso e sem erros, mas vamos ver o que acontece
quando você tenta criar outro espaço:
Oh céus. Parece que, ao passar uma entrada cuidadosamente elaborada, seu usuá-
rio conseguiu excluir totalmente a tabela de espaços e toda a sua rede social com
ela! A Figura 2.5 mostra o que o banco de dados viu quando você executou o pri-
meiro comando curl com o nome engraçado do proprietário. Como os valores de
entrada do usuário são concatenados no SQL como strings, o banco de dados
acaba vendo uma única string que parece conter duas instruções diferentes: a
INSERT instruçãopretendíamos, e uma DROP TABLE declaraçãoque o invasor
conseguiu injetar. O primeiro caractere do nome do proprietário é uma aspa sim-
ples, que fecha a aspa aberta inserida pelo nosso código. Os próximos dois carac-
teres são um parêntese de fechamento e um ponto e vírgula, que juntos garantem
que a INSERT instrução seja encerrada corretamente. A DROP TABLE instrução é
então inserida (injetada) após a INSERT instrução. Por fim, o invasor adiciona ou-
tro ponto e vírgula e dois hífens, que iniciam um comentário no SQL. Isso garante
que a aspa final e os parênteses inseridos pelo código sejam ignorados pelo banco
de dados e não causem um erro de sintaxe.
Figura 2.5 Um ataque de injeção de SQL ocorre quando a entrada do usuário é
misturada em uma instrução SQL sem que o banco de dados seja capaz de dife-
renciá-los. Para o banco de dados, esse comando SQL com um nome de proprietá-
rio engraçado acaba parecendo duas instruções separadas seguidas de um
comentário.
Quando esses elementos são colocados juntos, o resultado é que o banco de dados
vê duas instruções SQL válidas: uma que insere uma linha fictícia na tabela de es-
paços e outra que destrói essa tabela completamente. A Figura 2.6 é um desenho
animado famoso da história em quadrinhos XKCD que ilustra os problemas do
mundo real que a injeção de SQL pode causar.
Figura 2.6 As consequências de não lidar com ataques de injeção de SQL. (Crédito:
XKCD, “Exploits of a Mom,” https://www.xkcd.com/327/. )
Lásão algumas técnicas que você pode usar para prevenir ataques de injeção.
Você pode tentar escapar de quaisquer caracteres especiais na entrada para evi-
tar que tenham efeito. Nesse caso, por exemplo, talvez você possa escapar ou re-
mover as aspas simples. Essa abordagem geralmente é ineficaz porque bancos de
dados diferentes tratam caracteres diferentes de maneira especial e usam aborda-
gens diferentes para escapá-los. Pior ainda, o conjunto de caracteres especiais
pode mudar de lançamento para lançamento, então o que é seguro em um ponto
no tempo pode não ser tão seguro após uma atualização.
Figura 2.7 Uma instrução preparada garante que os valores de entrada do usuário
sejam sempre mantidos separados da própria instrução SQL. A instrução SQL con-
tém apenas espaços reservados (representados como pontos de interrogação) e é
analisada e compilada neste formulário. Os valores reais dos parâmetros são pas-
sados para o banco de dados separadamente, portanto, nunca pode ser confun-
dido ao tratar a entrada do usuário como código SQL a ser executado.
DEFINIÇÃO Uma declaração preparadaé uma instrução SQL com todas as en-
tradas do usuário substituídas por espaços reservados. Quando a instrução é exe-
cutada, os valores de entrada são fornecidos separadamente, garantindo que o
banco de dados nunca seja enganado para executar a entrada do usuário como
código.
A Listagem 2.6 mostra o createSpace código atualizado para usar uma instrução
preparada. Dalesbred tem suporte embutido para declarações preparadas sim-
plesmente escrevendo a declaração com valores de espaço reservado e incluindo
a entrada do usuário como argumentos extras para o updateUnique métodoli-
gar. Abra o arquivo SpaceController.java em seu editor de texto e localize o cre-
ateSpace método. Atualize o código para corresponder ao código da Listagem
2.6, usando uma instrução preparada em vez de concatenar strings manualmente.
Salve o arquivo quando estiver satisfeito com o novo código.
database.updateUnique(
"INSERT INTO spaces(space_id, name, owner) " + ❶
"VALUES(?, ?, ?);", spaceId, spaceName, owner); ❶
resposta.status(201);
response.header("Localização", "/espaços/" + spaceId);
retornar novo JSONObject()
.put("nome", espaçoNome)
.put("uri", "/espaços/" + spaceId);
});
Agora, quando sua instrução for executada, o banco de dados receberá a entrada
do usuário separadamente da consulta, impossibilitando que a entrada do usuá-
rio influencie os comandos executados. Vamos ver o que acontece quando você
executa sua chamada de API maliciosa. Desta vez, o espaço foi criado correta-
mente - embora com um nome engraçado!
Precisamos então atualizar nossa Main classepara alternar para o uso desse usuá-
rio restrito após o carregamento do esquema do banco de dados. Observe que não
podemos fazer isso antes que o esquema do banco de dados seja carregado, caso
contrário, não teríamos permissões suficientes para criar o banco de dados! Pode-
mos fazer isso simplesmente recarregando o DataSource objeto JDBCdepois de
criarmos o esquema, mudando para o novo usuário no processo. Localize e abra o
arquivo Main.java em seu editor novamente e navegue até o início do main méto-
doonde você inicializa o banco de dados. Altere as poucas linhas que criam e inici-
alizam o banco de dados para as seguintes linhas:
Aqui você cria e inicializa o banco de dados usando o usuário “natter” como an-
tes, mas depois recria o pool de conexões JDBC DataSource passando o nome de
usuário e a senha do usuário recém-criado. Em um projeto real, você deve usar
senhas mais seguras do que password , e verá como injetar senhas de conexão
mais seguras no capítulo 10.
Se quiser ver a diferença que isso faz, você pode reverter temporariamente as al-
terações feitas anteriormente para usar instruções preparadas. Se você tentar
executar o ataque de injeção SQL como antes, verá um erro 500. Mas desta vez, ao
verificar os logs, você verá que o ataque não foi bem-sucedido porque o DROP
TABLE comandofoi negado devido ainsuficientepermissões:
consulta de string =
"SELECT msg_text FROM messages WHERE author = '"
+ autor + "'"
qual será a saída da execução da consulta (supondo que a users tabela exista
com uma password coluna)?
1. Nada
2. Um erro de sintaxe
3. senha do João
4. As senhas de todos os usuários
5. Um erro de restrição de integridade
6. As mensagens escritas por João
7. Quaisquer mensagens escritas por John e as senhas de todos os usuários
As respostas estão no final do capítulo.
Desserialização insegura
Embora Java seja uma linguagem segura para memória e, portanto, menos pro-
pensa a ataques de estouro de buffer, isso não significa que ela seja imune a ata-
ques RCE. Algumas bibliotecas de serialização que convertem objetos Java arbitrá-
rios de e para strings ou formatos binários tornaram-se vulneráveis a ataques
RCE, conhecidos como vulnerabilidade de desserialização insegura Serializa-
ble no OWASP Top 10. Isso afeta a estrutura interna do Java, mas também anali-
sadores para formatos supostamente seguros como JSON têm sido vulneráveis,
como o popular Jackson Databind. a O problema ocorre porque o Java executará o
código dentro do construtor padrão de qualquer objeto sendo desserializado por
essas estruturas.
Consulte https://adamcaudill.com/2017/10/04/exploiting-jackson-rce-cve-2017-7525/
para obter uma descrição da vulnerabilidade. A vulnerabilidade depende de um
recurso do Jackson que está desabilitado por padrão.
Embora a API esteja usando um analisador JSON seguro, ela ainda confia na en-
trada em outros aspectos. Por exemplo, ele não verifica se o nome de usuário for-
necido é menor que o máximo de 30 caracteres configurado no esquema do banco
de dados. O que acontece quando você passa um nome de usuário mais longo?
$ curl -d '{"name":"test", "owner":"um nome de usuário muito longo
➥ com mais de 30 caracteres"}'
➥ http://localhost:4567/spaces -i
Erro do servidor HTTP/1.1 500
Data: sexta-feira, 01 de fevereiro de 2019 13:28:22 GMT
Tipo de conteúdo: aplicativo/json
Codificação de transferência: em partes
Servidor: Jetty(9.4.8.v20171121)
Se você examinar os logs do servidor, verá que a restrição do banco de dados de-
tectou o problema:
Mas você não deve confiar no banco de dados para detectar todos os erros. Um
banco de dados é um ativo valioso que sua API deve proteger contra solicitações
inválidas. O envio de solicitações ao banco de dados que contêm erros básicos
apenas vincula recursos que você preferiria usar para processar solicitações ge-
nuínas. Além disso, pode haver restrições adicionais que são mais difíceis de ex-
pressar em um esquema de banco de dados. Por exemplo, você pode exigir que o
usuário exista no diretório LDAP corporativo. Na listagem 2.8, você adicionará al-
guma validação de entrada básica para garantir que os nomes de usuário tenham
no máximo 30 caracteres e os nomes de espaço até 255 caracteres. Você também
garantirá que os nomes de usuário contenham apenas caracteres alfanuméricos,
usando uma expressão regular.
PRINCÍPIO Sempre defina entradas aceitáveisem vez de inaceitávelao validar
entradas não confiáveis. Uma lista de permissõesdescreve exatamente quais en-
tradas são consideradas válidas e rejeita qualquer outra coisa. 1 Uma lista de
bloqueio(ou lista de negações), por outro lado, tenta descrever quais entradas são
inválidas e aceita qualquer outra coisa. As listas de bloqueio podem levar a falhas
de segurança se você não antecipar todas as entradas maliciosas possíveis. Onde o
intervalo de entradas pode ser grande e complexo, como texto Unicode, considere
listar classes gerais de entradas aceitáveis como “dígito decimal” em vez de valo-
res de entrada individuais.
[a-zA-Z][a-zA-Z0-9]{1,29}
❷ Aqui usamos uma expressão regular para garantir que o nome de usuário seja
válido.
As expressões regulares são uma ferramenta útil para validação de entrada, por-
que podem expressar de forma sucinta restrições complexas na entrada. Nesse
caso, a expressão regular garante que o nome de usuário consista apenas em ca-
racteres alfanuméricos, não comece com um número e tenha entre 2 e 30 caracte-
res de comprimento. Embora poderosas, as próprias expressões regulares podem
ser uma fonte de ataque. Algumas implementações de expressões regulares po-
dem ser feitas para consumir grandes quantidades de tempo de CPU ao processar
certas entradas, levando a um ataque conhecido comoataque de negação de ser-
viço de expressão regular (ReDoS)(veja a barra lateral).
Ataques ReDoS
Uma negação de serviço de expressão regular (ou ReDoS) ocorre quando uma ex-
pressão regular pode ser forçada a levar muito tempo para corresponder a uma
string de entrada cuidadosamente escolhida. Isso pode acontecer se a implemen-
tação da expressão regular for forçada a retroceder várias vezes para considerar
as diferentes formas possíveis de correspondência da expressão.
O Java 11 parece ser menos suscetível a esses ataques do que as versões anteriores.
Se você compilar e executar esta nova versão da API, descobrirá que ainda obtém
um erro 500, mas pelo menos não está mais enviando solicitações inválidas ao
banco de dados. Para comunicar um erro mais descritivo ao usuário, você pode
instalar um manipulador de exceção Spark em sua Main classe, como mostra a
Listagem 2.9. Volte para o arquivo Main.java em seu editor e navegue até o final
do método main. Os manipuladores de exceção do Spark são registrados cha-
mando o Spark.exception() método, que já importamos estaticamente. O mé-
todo recebe dois argumentos: a classe de exceção para manipular e, em seguida,
uma função de manipulador que receberá a exceção, a solicitação e os objetos de
resposta. A função do manipulador pode usar o objeto de resposta para produzir
uma mensagem de erro apropriada. Neste caso, você vai pegar IllegalArgumen-
tException lançado pelo nosso código de validação, e JSONException lançado
pelo analisador JSON quando recebe uma entrada incorreta. Em ambos os casos,
você pode usar um método auxiliar para retornar um erro 400 Bad Request for-
matado ao usuário. Você também pode retornar um resultado 404 Not Found
quando um usuário tentar acessar um espaço que não existe capturando o
Dalesbred's EmptyResultException .
import org.dalesbred.result.EmptyResultException; ❶
import spark.*; ❶
questionário
DentroAlém de validar todas as entradas, uma API também deve tomar cuidado
para garantir que as saídas que ela produz sejam bem formadas e não possam ser
abusadas. Infelizmente, o código que você escreveu até agora não cuida desses de-
talhes. Vamos dar uma olhada novamente na saída que você acabou de produzir:
Script entre sites, ou XSS, é uma vulnerabilidade comum que afeta aplicativos da
Web, em que um invasor pode fazer com que um script seja executado no con-
texto de outro site. Em um XSS persistente, o script é armazenado em dados no
servidor e executado sempre que um usuário acessa esses dados por meio do apli-
cativo da web. Um refletidoO XSS ocorre quando uma entrada criada com códigos
maliciosos para uma solicitação faz com que o script seja incluído (refletido) na
resposta a essa solicitação. O XSS refletido é um pouco mais difícil de explorar
porque a vítima precisa ser induzida a visitar um site sob o controle do invasor
para acionar o ataque. Um terceiro tipo de XSS, conhecido como XSS baseado em
DOM, ataca o código JavaScript que cria HTML dinamicamente no navegador.
Paraentender o ataque XSS, vamos tentar explorá-lo. Antes de fazer isso, talvez
seja necessário adicionar um cabeçalho especial à sua resposta para desativar as
proteções integradas em alguns navegadores que detectarão e impedirão ataques
XSS refletidos. Essa proteção costumava ser amplamente implementada em nave-
gadores, mas foi recentemente removida do Chrome e do Microsoft Edge. 2 Se
você estiver usando um navegador que ainda o implemente, essa proteção torna
mais difícil realizar esse ataque específico, então você o desativará adicionando o
seguinte filtro de cabeçalho à sua Main classe(um afterAfter filtrono Spark é
executado após todos os outros filtros, incluindo manipuladores de exceção). Abra
o arquivo Main.java em seu editor e adicione as seguintes linhas ao final do mé-
todo main:
afterAfter((solicitação, resposta) -> {
response.header("X-XSS-Protection", "0");
});
Feito isso, você pode criar um arquivo HTML malicioso que explora o bug. Abra
seu editor de texto e crie um arquivo chamado xss.html e copie o conteúdo da lis-
tagem 2.10 para ele. Salve o arquivo e clique duas vezes nele ou abra-o em seu na-
vegador. O arquivo inclui um formulário HTML com o enctype atributodefinido
para text/plain . Isso instrui o navegador da Web a formatar os campos no for-
mulário como field=value pares de texto simples, que você está explorando
para fazer com que a saída pareça um JSON válido. Você também deve incluir um
pequeno pedaço de JavaScript para enviar automaticamente o formulário assim
que a página for carregada.
<!DOCTYPE html>
<html>
<corpo>
<form id="test" action="http://localhost:4567/spaces"
method="post" enctype="text/plain"> ❶
<input type="hidden" name='{"x":"'
valor='","nome":"x",
➥ "owner":"<script>alert('XSS!');
➥ </script>"}' /> ❷
</form>
<tipo de script="texto/javascript">
document.getElementById("teste").submit(); ❸
</script>
</body>
</html>
Se tudo correr como esperado, você deve receber um pop-up em seu navegador
com a mensagem “XSS”. Então o que aconteceu? A sequência de eventos é mos-
trada na figura 2.8, e é a seguinte:
{"x":"=","name":"x","owner":"<script>alert('XSS!');</script>"}
3. A API vê uma entrada JSON válida e ignora o campo “x” extra (que você adicio-
nou apenas para ocultar habilmente o sinal de igual que o navegador inseriu).
Mas a API rejeita o nome de usuário como inválido, repetindo-o na resposta:
4. Como sua resposta de erro foi exibida com o tipo de conteúdo padrão de
text/html , o navegador interpreta alegremente a resposta como HTML e exe-
cuta o script, resultando no pop-up XSS.
Figura 2.8 Um script cross-site refletido (XSS) contra sua API pode ocorrer quando
um invasor faz com que um cliente de navegador da Web envie um formulário
com campos de entrada cuidadosamente elaborados. Quando enviado, o formulá-
rio parece um JSON válido para a API, que o analisa, mas produz uma mensagem
de erro. Como a resposta é retornada incorretamente com um tipo de conteúdo
HTML, o script mal-intencionado fornecido pelo invasor é executado pelo cliente
do navegador da web.
Então,como você conserta isso? Existem várias etapas que podem ser tomadas
para evitar que sua API seja usada para iniciar ataques XSS contra clientes de na-
vegadores da web:
Seja rigoroso no que você aceita. Se sua API consumir entrada JSON, exija que
todas as solicitações incluam um Content-Type cabeçalhodefinido para
application/json . Isso evita os truques de envio de formulário usados neste
exemplo, pois um formulário HTML não pode enviar application/json con-
teúdo.
Certifique-se de que todas as saídas estejam bem formadas usando uma biblio-
teca JSON adequada, em vez de concatenar strings.
Produza Content-Type cabeçalhos corretos em todas as respostas da sua API e
nunca assuma que os padrões são sensatos. Verifique as respostas de erro em
particular, pois elas geralmente são configuradas para produzir HTML por
padrão.
Se você analisar o Accept cabeçalhopara decidir que tipo de saída produzir,
nunca simplesmente copie o valor desse cabeçalho na resposta. Sempre especi-
fique explicitamente o Content-Type que sua API produziu.
Além disso, existem alguns cabeçalhos de segurança padrão que você pode adicio-
nar a todas as respostas da API para adicionar proteção adicional aos clientes do
navegador da Web (consulte a tabela 2.1).
Tabela 2.1 Cabeçalhos de segurança úteis
Vocêagora deve atualizar a API para implementar essas proteções. Você adicio-
nará alguns filtros executados antes e depois de cada solicitação para impor as
configurações de segurança recomendadas.
Primeiro, adicione um before () filtro que é executado antes de cada solicitação
e verifica se qualquer corpo POST enviado à API possui um cabeçalho Content-
Type correto de application/ json . A API Natter só aceita entrada de solicita-
ções POST, mas se sua API lida com outros métodos de solicitação que podem con-
ter um corpo (como solicitações PUT ou PATCH), você também deve aplicar esse
filtro para esses métodos. Se o tipo de conteúdo estiver incorreto, você deve retor-
nar um status 415 Unsupported Media Type, porque este é o código de status pa-
drão para este caso. Você também deve indicar explicitamente a codificação de ca-
racteres UTF-8 na resposta, para evitar truques para roubar dados JSON especifi-
cando uma codificação diferente, como UTF-16BE (consulte
https://portswigger.net/blog/json-hijacking-for -the-modern-web para detalhes).
Em segundo lugar, você adicionará um filtro que será executado após todas as so-
licitações para adicionar nossos cabeçalhos de segurança recomendados à res-
posta. Você adicionará isso como um Spark afterAfter () filter, que garante
que os cabeçalhos sejam adicionados às respostas de erro, bem como às respostas
normais.
A Listagem 2.11 mostra seu método principal atualizado, incorporando essas me-
lhorias. Localize o arquivo Main.java em natter-api/src/main/java/com/manning/
apisecurityinaction e abra-o em seu editor. Adicione os filtros ao main() méto-
doabaixo do código que você já escreveu.
internalServerError(novo JSONObject()
.put("erro", "erro interno do servidor").toString());
notFound(novo JSONObject()
.put("erro", "não encontrado").toString());
exceção(IllegalArgumentException.class, Main::badRequest);
exceção(JSONException.class, Main::badRequest);
}
❷ Retorne uma resposta padrão 415 Unsupported Media Type para Content-Types
inválidos.
Você também deve alterar suas exceções para não repetir a entrada do usuário
malformada em nenhum caso. Embora os cabeçalhos de segurança devam evitar
quaisquer efeitos ruins, é melhor não incluir a entrada do usuário nas respostas
de erro apenas para ter certeza. É fácil remover acidentalmente um cabeçalho de
segurança, então você deve evitar o problema retornando uma mensagem de erro
mais genérica:
if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{0,29}")) {
throw new IllegalArgumentException("nome de usuário inválido");
}
4. Qual cabeçalho de segurança deve ser usado para impedir que os navegadores
da Web ignorem o Content-Type cabeçalho em uma resposta?
1. Cache-Control
2. Content-Security-Policy
3. X-Frame-Options: deny
4. X-Content-Type-Options: nosniff
5. X-XSS-Protection: 1; mode=block
5. Suponha que sua API possa produzir saída no formato JSON ou XML, de acordo
com o Accept cabeçalho enviado pelo cliente. Qual das seguintes opções você
não deve fazer? (Pode haver mais de uma resposta correta.)
1. Defina o X-Content-Type-Options cabeçalho.
2. Inclua valores de entrada não sanitizados em mensagens de erro.
3. Produza saída usando uma biblioteca JSON ou XML bem testada.
4. Certifique-se de que o tipo de conteúdo esteja correto em todas as respostas
de erro padrão.
5. Copie o Accept cabeçalho diretamente para o Content-Type cabeçalho na
resposta.
1. e. Cross-Site Request Forgery (CSRF) esteve no Top 10 por muitos anos, mas per-
deu importância devido a defesas aprimoradas em estruturas da web. Ataques
e defesas CSRF são abordados no capítulo 4.
2. g. As mensagens de John e as senhas de todos os usuários serão retornadas da
consulta. Isso é conhecido como ataque UNION de injeção SQL e mostra que um
invasor não está limitado a recuperar dados das tabelas envolvidas na consulta
original, mas também pode consultar outras tabelas no banco de dados.
3. b. O invasor pode fazer com que o programa aloque matrizes de bytes grandes
com base na entrada do usuário. int Para um valor Java, o máximo seria um
array de 2 GB, o que provavelmente permitiria ao invasor esgotar toda a memó-
ria disponível com algumas solicitações. Embora a passagem de valores inváli-
dos seja um aborrecimento, lembre-se do início da seção 2.5 que Java é uma lin-
guagem de memória segura e, portanto, isso resultará em exceções em vez de
comportamento inseguro.
4. d. X-Content-Type-Options: nosniff instrui os navegadores a respeitar o
cabeçalho Content-Type na resposta.
5. b e e. Você nunca deve incluir valores de entrada não sanitizados em mensa-
gens de erro, pois isso pode permitir que um invasor injete scripts XSS. Você
também nunca deve copiar o cabeçalho Accept da solicitação para o cabeçalho
Content-Type de uma resposta, mas, em vez disso, construí-lo do zero com base
no tipo de conteúdo real quefoiproduzido.
Resumo
1.
Você pode ouvir os termos mais antigos lista branca e lista negra usados para es-
ses conceitos, mas essas palavras podem ter conotações negativas e devem ser
evitadas. Consulte https://www.ncsc.gov.uk/blog-post/terminology-its-not-black-
and-white para uma discussão.
2.
Consulte https://scotthelme.co.uk/edge-to-remove-xss-auditor/ para uma discussão
sobre as implicações do anúncio da Microsoft. O Firefox nunca implementou as
proteções em primeiro lugar, então essa proteção logo desaparecerá da maioria
dos principais navegadores. No momento da redação deste artigo, o Safari era o
único navegador que bloqueava o ataque por padrão.