Você está na página 1de 66

Tópicos Comece a aprender Pesquise mais de 50.

000 cursos, even… O que há de novo

3 Protegendo a API do Natter


Este capítulocapas

Autenticando usuários com autenticação básica HTTP


Autorizando solicitações com listas de controle de acesso
Garantindo a responsabilidade por meio do log de auditoria
Mitigação de ataques de negação de serviço com limitação de taxa

No último capítulo você aprendeu como desenvolver a funcionalidade de sua API


evitando falhas de segurança comuns. Neste capítulo, você irá além da funcionali-
dade básica e verá como mecanismos de segurança proativos podem ser adiciona-
dos à sua API para garantir que todas as solicitações sejam de usuários genuínos e
devidamente autorizados. Você protegerá a API Natter desenvolvida no capítulo 2,
aplicando autenticação de senha eficaz usando Scrypt, bloqueando comunicações
com HTTPS e evitando ataques de negação de serviço usando a biblioteca de limi-
tação de taxa Guava.

3.1 Lidando com ameaças com controles de segurança

Você vaiproteja a API do Natter contra ameaças comuns aplicando alguns meca-
nismos básicos de segurança (também conhecidos como controles de segurança).
A Figura 3.1 mostra os novos mecanismos que você desenvolverá e poderá relaci-
onar cada um deles a uma ameaça STRIDE (capítulo 1) que eles impedem:
A limitação de taxa é usada para evitar que os usuários sobrecarreguem sua
API com solicitações, limitando as ameaças de negação de serviço.
Criptografiagarante que os dados sejam mantidos em sigilo quando enviados de
ou para a API e quando armazenados em disco, evitando a divulgação de infor-
mações. A criptografia moderna também evita que os dados sejam adulterados.
A autenticação garante que os usuários sejam quem dizem ser, evitando falsifi-
cações. Isso é essencial para a responsabilidade, mas também uma base para
outros controles de segurança.
O registro de auditoria é a base da responsabilidade, para evitar ameaças de
repúdio.
Por fim, você aplicará o controle de acesso para preservar a confidencialidade e
a integridade, evitando a divulgação de informações, adulteração e ataques de
elevação de privilégio.
Figura 3.1 Aplicando controles de segurança à API do Natter. A criptografia im-
pede a divulgação de informações. A limitação de taxa protege a disponibilidade.
A autenticação é usada para garantir que os usuários sejam quem dizem ser. O log
de auditoria registra quem fez o quê, para apoiar a responsabilidade. O controle
de acesso é então aplicado para reforçar a integridade e a confidencialidade.

NOTA Um detalhe importante, mostrado na figura 3.1, é que apenas a limitação


de taxa e o controle de acesso rejeitam diretamente as solicitações. Uma falha na
autenticação não causa imediatamente a falha de uma solicitação, mas uma deci-
são de controle de acesso posterior pode rejeitar uma solicitação se ela não for au-
tenticada. Isso é importante porque queremos garantir que até mesmo as solicita-
ções com falha sejam registradas, o que não aconteceria se o processo de autenti-
cação rejeitasse imediatamente as solicitações não autenticadas.
Juntos, esses cinco controles básicos de segurança abordam as seis ameaças bási-
cas do STRIDE de falsificação, adulteração, repúdio, divulgação de informações,
negação de serviço e elevação de privilégio que foram discutidas no capítulo 1.
Cada controle de segurança é discutido e implementado no restante destecapítulo.

3.2 Limitação de taxa para disponibilidade

Ameaçascontra a disponibilidade, como negação de serviço (DoS) ataques, pode


ser muito difícil de prevenir completamente. Esses ataques geralmente são execu-
tados usando recursos de computação sequestrados, permitindo que um invasor
gere grandes quantidades de tráfego com pouco custo para si mesmo. A defesa
contra um ataque DoS, por outro lado, pode exigir recursos significativos, cus-
tando tempo e dinheiro. Mas há várias etapas básicas que você pode seguir para
reduzir a oportunidade de ataques DoS.

DEFINIÇÃO Uma negação de serviço (DoS) visa impedir que usuários legítimos
acessem sua API. Isso pode incluir ataques físicos, como desconectar cabos de
rede, mas geralmente envolve a geração de grandes quantidades de tráfego para
sobrecarregar seus servidores. Um ataque DoS distribuído (DDoS)usa muitas má-
quinas na internet para gerar tráfego, tornando-o mais difícil de bloquear do que
um único cliente ruim.

Muitos ataques DoS são causados ​por solicitações não autenticadas. Uma maneira
simples de limitar esses tipos de ataques é nunca permitir que solicitações não au-
tenticadas consumam recursos em seus servidores. A autenticação é abordada na
seção 3.3 e deve ser aplicada imediatamente após a limitação de taxa antes de
qualquer outro processamento. No entanto, a autenticação em si pode ser cara,
então isso não elimina as ameaças DoS por conta própria.
OBSERVAÇÃO Nunca permita que solicitações não autenticadas consumam re-
cursos significativos em seu servidor.

Muitos ataques DDoS dependem de alguma forma de amplificação para que uma
solicitação não autenticada a uma API resulte em uma resposta muito maior que
pode ser direcionada ao alvo real. Um exemplo popular são os ataques de amplifi-
cação de DNS, que aproveitam o sistema de nome de domínio não
autenticado(DNS) que mapeia nomes de host e domínio em endereços IP. Ao falsi-
ficar o endereço de retorno para uma consulta de DNS, um invasor pode enganar
o servidor DNS para inundar a vítima com respostas a solicitações de DNS que
nunca foram enviadas. Se servidores DNS suficientes puderem ser recrutados
para o ataque, uma quantidade muito grande de tráfego poderá ser gerada a par-
tir de uma quantidade muito menor de tráfego de solicitação, conforme mostrado
na figura 3.2. Ao enviar solicitações de uma rede de máquinas comprometidas
(conhecida como botnet), o invasor pode gerar quantidades muito grandes de trá-
fego para a vítima com pouco custo para si. A amplificação de DNS é um exemplo
de ataque DoS em nível de rede. Esses ataques podem ser mitigados filtrando o
tráfego prejudicial que entra em sua rede usando um firewall. Ataques muito
grandes muitas vezes só podem ser tratados por serviços especializados de prote-
ção DoS fornecidos por empresas que têm capacidade de rede suficiente para li-
dar com a carga.
Figura 3.2 Em um ataque de amplificação de DNS, o invasor envia a mesma con-
sulta de DNS para vários servidores DNS, falsificando seu endereço IP para pare-
cer que a solicitação veio da vítima. Ao escolher cuidadosamente a consulta de
DNS, o servidor pode ser induzido a responder com muito mais dados do que na
consulta original, inundando a vítima com tráfego.

DICA Ataques de amplificação geralmente exploram pontos fracos em protocolos


baseados em UDP (User Datagram Protocol), que são populares na Internet das
Coisas(IoT). A proteção de APIs de IoT é abordada nos capítulos 12 e 13.

Os ataques DoS no nível da rede podem ser fáceis de detectar porque o tráfego
não está relacionado a solicitações legítimas à sua API. Ataques DoS na camada de
aplicativostente sobrecarregar uma API enviando solicitações válidas, mas em ta-
xas muito mais altas do que um cliente normal. Uma defesa básica contra ataques
de negação de serviço na camada de aplicativos é aplicar a limitação de taxa a to-
das as solicitações, garantindo que você nunca tente processar mais solicitações
do que o seu servidor pode suportar. É melhor rejeitar alguns pedidos neste caso,
do que travar tentando processar tudo. Clientes genuínos podem repetir suas soli-
citações mais tarde, quando o sistema voltar ao normal.
DEFINIÇÃO Ataques DoS na camada de aplicativos(também conhecido como
camada 7 ou L7 DoS) envia solicitações sintaticamente válidas para sua API, mas
tenta sobrecarregá-la enviando um volume muito grande de solicitações.

A limitação de taxa deve ser a primeira decisão de segurança tomada quando


uma solicitação chega à sua API. Como o objetivo da limitação de taxa é garantir
que sua API tenha recursos suficientes para poder processar solicitações aceitas,
você precisa garantir que as solicitações que excedem as capacidades de sua API
sejam rejeitadas rapidamente e muito cedo no processamento. Outros controles
de segurança, como autenticação, podem usar recursos significativos, portanto, a
limitação de taxa deve ser aplicada antes desses processos, conforme mostrado na
figura 3.3.

Figura 3.3 A limitação de taxa rejeita solicitações quando sua API está sob carga
excessiva. Ao rejeitar as solicitações antes que elas tenham consumido muitos re-
cursos, podemos garantir que as solicitações que processamos tenham recursos
suficientes para serem concluídas sem erros. A limitação de taxa deve ser a pri-
meira decisão aplicada às solicitações recebidas.

DICA Você deve implementar a limitação de taxa o mais cedo possível, ideal-
mente em um balanceador de carga ou proxy reverso antes mesmo que as solici-
tações cheguem aos seus servidores de API. A configuração de limitação de taxa
varia de produto para produto. Consulte https://medium
.com/faun/understanding-rate-limiting-on-haproxy-b0cf500310b1 para obter um
exemplo de configuração de limitação de taxa para o balanceador de carga
HAProxy de código aberto.

3.2.1 Limitação de taxa com goiaba

Muitas vezesa limitação de taxa é aplicada em um proxy reverso, gateway de API


ou balanceador de carga antes que a solicitação chegue à API, para que possa ser
aplicada a todas as solicitações que chegam a um cluster de servidores. Ao lidar
com isso em um servidor proxy, você também evita que o excesso de carga seja
gerado em seus servidores de aplicativos. Neste exemplo, você aplicará limitação
de taxa simples no próprio servidor de API usando a biblioteca Guava do Google.
Mesmo se você impor limitação de taxa em um servidor proxy, é uma boa prática
de segurança também impor limites de taxa em cada servidor para que, se o ser-
vidor proxy se comportar mal ou estiver mal configurado, ainda seja difícil desati-
var os servidores individuais. Este é um exemplo do princípio geral de segurança
conhecido como defesa em profundidade, que visa garantir que nenhuma falha
de um único mecanismo seja suficiente para comprometer sua API.

DEFINIÇÃO O princípio da defesa em profundidadeafirma que várias camadas


de defesas de segurança devem ser usadas para que uma falha em qualquer uma
das camadas não seja suficiente para violar a segurança de todo o sistema.
Como você descobrirá agora, existem bibliotecas disponíveis para tornar a limita-
ção de taxa básica muito fácil de adicionar à sua API, enquanto requisitos mais
complexos podem ser atendidos com produtos de proxy/gateway prontos para
uso. Abra o arquivo pom.xml em seu editor e adicione a seguinte dependência à
seção de dependências:

<dependência>
<groupId>com.google.guava</groupId>
<artifactId>goiaba</artifactId>
<version>29.0-jre</version>
</dependência>

Guava torna muito simples implementar a limitação de taxa usando a RateLimi-


ter classeque nos permite definir a taxa de solicitações por segundo que você de-
seja permitir. 1 Você pode então bloquear e aguardar até que a taxa reduza ou
simplesmente rejeitar a solicitação, como fazemos na listagem a seguir. O código
de status HTTP 429 Too Many Requests padrão 2 pode ser usado para indicar que
a limitação de taxa foi aplicada e que o cliente deve tentar a solicitação nova-
mente mais tarde. Você também pode enviar um Retry-After cabeçalhopara in-
dicar quantos segundos o cliente deve esperar antes de tentar novamente. Defina
um limite baixo de 2 solicitações por segundo para facilitar a visualização em
ação. O limitador de taxa deve ser o primeiro filtro definido em seu método prin-
cipal, porque mesmo a autenticação e o log de auditoria podem consumir
recursos.

DICA O limite de taxa para servidores individuais deve ser uma fração do limite
de taxa geral que você deseja que seu serviço manipule. Se o seu serviço precisar
lidar com mil solicitações por segundo e você tiver 10 servidores, o limite de taxa
por servidor deve ser de cerca de 100 solicitações por segundo. Você deve verifi-
car se cada servidor é capaz de lidar com essa taxa máxima.

Abra o arquivo Main.java em seu editor e adicione uma importação para Guava
no topo do arquivo:

import com.google.common.util.concurrent.*;

Em seguida, no método main, após inicializar o banco de dados e construir os ob-


jetos controladores, adicione o código da listagem 3.1 para criar o RateLimite-
r objetoe adicione um filtro para rejeitar qualquer solicitação quando o limite de
taxa for excedido. Usamos o tryAcquire() método sem bloqueioque retorna
false se a solicitação deve ser rejeitada.

Listagem 3.1 Aplicando limitação de taxa com goiaba

var rateLimiter = RateLimiter.create(2.0d); ❶

before((pedido, resposta) -> {


if (!rateLimiter.tryAcquire()) { ❷
response.header("Retry-After", "2"); ❸
parada(429); ❹
}
});

❶ Crie o objeto limitador de taxa compartilhada e permita apenas 2 solicitações de


API por segundo.

❷ Verifique se a taxa foi excedida.


❸ Em caso afirmativo, adicione um cabeçalho Retry-After indicando quando o cli-
ente deve tentar novamente.

❹ Retorne um status 429 Too Many Requests.

O limitador de taxa do Guava é bastante básico, definindo apenas uma taxa sim-
ples de requisições por segundo. Possui recursos adicionais, como poder consu-
mir mais licenças para operações de API mais caras. Ele carece de recursos mais
avançados, como ser capaz de lidar com rajadas ocasionais de atividade, mas é
perfeitamente adequado como uma medida defensiva básica que pode ser incor-
porada a uma API em algumas linhas de código. Você pode experimentá-lo na li-
nha de comando para vê-lo em ação:

$ para i em {1..5}
> fazer
> curl -i -d "{\"proprietário\":\"teste\",\"nome\":\"espaço$i\"}"
➥ -H 'Tipo de conteúdo: application/json'
➥ http://localhost:4567/spaces;
> feito
HTTP/1.1 201 Criado ❶
Data: quarta-feira, 06 de fevereiro de 2019 21:07:21 GMT
Localização: /espaços/1
Tipo de conteúdo: application/json;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Opções: NEGAR
X-XSS-Proteção: 0
Cache-Control: no-store
Política de segurança de conteúdo: default-src 'nenhum'; frame-ancestral 'nenhum'; caixa
Servidor:
Codificação de transferência: em partes
HTTP/1.1 201 Criado ❶
Data: quarta-feira, 06 de fevereiro de 2019 21:07:21 GMT
Localização: /espaços/2
Tipo de conteúdo: application/json;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Opções: NEGAR
X-XSS-Proteção: 0
Cache-Control: no-store
Política de segurança de conteúdo: default-src 'nenhum'; frame-ancestral 'nenhum'; caixa
Servidor:
Codificação de transferência: em partes

HTTP/1.1 201 Criado ❶


Data: quarta-feira, 06 de fevereiro de 2019 21:07:22 GMT
Localização: /espaços/3
Tipo de conteúdo: application/json;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Opções: NEGAR
X-XSS-Proteção: 0
Cache-Control: no-store
Política de segurança de conteúdo: default-src 'nenhum'; frame-ancestral 'nenhum'; caixa
Servidor:
Codificação de transferência: em partes
HTTP/1.1 429 Muitos pedidos ❷
Data: quarta-feira, 06 de fevereiro de 2019 21:07:22 GMT
Tipo de conteúdo: application/json;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Opções: NEGAR
X-XSS-Proteção: 0
Cache-Control: no-store
Política de segurança de conteúdo: default-src 'nenhum'; frame-ancestral 'nenhum'; caixa
Servidor:
Codificação de transferência: em partes

HTTP/1.1 429 Muitos pedidos ❷


Data: quarta-feira, 06 de fevereiro de 2019 21:07:22 GMT
Tipo de conteúdo: application/json;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Opções: NEGAR
X-XSS-Proteção: 0
Cache-Control: no-store
Política de segurança de conteúdo: default-src 'nenhum'; frame-ancestral 'nenhum'; caixa
Servidor:
Codificação de transferência: em partes

❶ As primeiras solicitações são bem-sucedidas enquanto o limite de taxa não é


excedido.

❷ Uma vez excedido o limite de taxa, as solicitações são rejeitadas com um código de
status 429.

Ao retornar uma resposta 429 imediatamente, você pode limitar a quantidade de


trabalho que sua API está realizando ao mínimo necessário, permitindo que ela
use esses recursos para atender às solicitações com as quais pode lidar. O limite
de taxa deve sempre ser definido abaixo do que você acha que seus servidores po-
dem suportar, para dar alguma manobraquarto.

questionário

1. Qual das seguintes afirmações é verdadeira sobre limitação de frequência?


1. A limitação de taxa deve ocorrer após o controle de acesso.
2. A limitação de taxa interrompe todos os ataques de negação de serviço.
3. A limitação da taxa deve ser aplicada o mais cedo possível.
4. A limitação de taxa só é necessária para APIs com muitos clientes.
2. Qual cabeçalho de resposta HTTP pode ser usado para indicar quanto tempo
um cliente deve esperar antes de enviar mais solicitações?
1. Expira
2. Repetir-Após
3. Última modificação
4. Política de segurança de conteúdo
5. Acesso-Controle-Max-Idade

As respostas estão no final do capítulo.

3.3 Autenticação para evitar spoofing

Quasetodas as operações em nossa API precisam saber quem as está executando.


Quando você fala com um amigo na vida real, você o reconhece com base em sua
aparência e características físicas. No mundo online, essa identificação instantâ-
nea geralmente não é possível. Em vez disso, contamos com as pessoas para nos
dizer quem são. Mas e se eles não forem honestos? Para um aplicativo social, os
usuários podem se passar por outros para espalhar boatos e fazer amigos se de-
sentenderem. Para uma API bancária, seria catastrófico se os usuários pudessem
facilmente fingir ser outra pessoa e gastar seu dinheiro. Quase toda a segurança
começa com a autenticação, que é o processo de verificação de que um usuário é
quem diz ser.

A Figura 3.4 mostra como a autenticação se encaixa nos controles de segurança


que você adicionará à API neste capítulo. Além da limitação de taxa (que é apli-
cada a todas as solicitações, independentemente de sua origem), a autenticação é
o primeiro processo que realizamos. Os controles de segurança downstream,
como registro de auditoria e controle de acesso, quase sempre precisam saber
quem é o usuário. É importante perceber que a própria fase de autenticação não
deve rejeitar uma solicitação, mesmo que a autenticação falhe. Decidir se uma de-
terminada solicitação exige que o usuário seja autenticado é tarefa do controle de
acesso (abordado mais adiante neste capítulo), e sua API pode permitir que algu-
mas solicitações sejam executadas anonimamente. Em vez de,

Figura 3.4 A autenticação ocorre após a limitação de taxa, mas antes do log de au-
ditoria ou controle de acesso. Todas as solicitações continuam, mesmo se a auten-
ticação falhar, para garantir que sejam sempre registradas. Solicitações não au-
tenticadas serão rejeitadas durante o controle de acesso, que ocorre após o log de
auditoria.

Na API do Natter, um usuário faz uma declaração de identidade em dois locais:

1. Na operação Create Space, a requisição inclui um campo “owner” que identifica


o usuário que está criando o espaço.
2. Na operação Postar Mensagem, o usuário se identifica no campo “autor”.

As operações para ler mensagens atualmente não identificam quem está solici-
tando essas mensagens, o que significa que não podemos dizer se eles devem ter
acesso. Você corrigirá ambos os problemas introduzindo a autenticação.

3.3.1 Autenticação básica HTTP

LáExistem muitas maneiras de autenticar um usuário, mas uma das mais difundi-
das é a autenticação simples de nome de usuário e senha. Em um aplicativo da
Web com uma interface de usuário, podemos implementar isso apresentando ao
usuário um formulário para inserir seu nome de usuário e senha. Uma API não é
responsável por renderizar uma interface do usuário, portanto, você pode usar o
mecanismo de autenticação HTTP básico padrão para solicitar uma senha de uma
forma que não dependa de nenhuma interface do usuário. Este é um esquema pa-
drão simples, especificado no RFC 7617 ( https://tools.ietf.org/html/rfc7617 ), no
qual o nome de usuário e a senha são codificados (usando a codificação Base64;
https://en.wikipedia.org/ wiki/Base64 ) e enviado em um cabeçalho. Um exemplo
de um cabeçalho de autenticação básica para o nome de usuário demo e a senha
changeit é o seguinte:

Autorização: Básico ZGVtbzpjaGFuZ2VpdA==

O cabeçalho Authorization é um cabeçalho HTTP padrão para enviar credenciais


ao servidor. É extensível, permitindo diferentes esquemas de autenticação, 3 mas
neste caso você está usando o esquema Básico. As credenciais seguem o identifica-
dor do esquema de autenticação. Para autenticação básica, eles consistem em
uma string do nome de usuário seguido por dois pontos 4 e, em seguida, a senha.
A string é então convertida em bytes (geralmente em UTF-8, mas o padrão não es-
pecifica) e codificada em Base64, que você pode ver se decodificá-la emjshell:

jshell> nova String(


java.util.Base64.getDecoder().decode("ZGVtbzpjaGFuZ2VpdA=="), "UTF-8")
$3 ==> "demo:changeit"

AVISO AS credenciais básicas do HTTP são fáceis de decodificar para qualquer


pessoa capaz de ler mensagens de rede entre o cliente e o servidor. Você só deve
enviar senhas por meio de uma conexão criptografada. Você adicionará criptogra-
fia às comunicações da API na seção 3.4.

3.3.2 Armazenamento seguro de senhas com Scrypt

Redeos navegadores têm suporte embutido para autenticação HTTP Basic (em-
bora com algumas peculiaridades que você verá mais tarde), assim como o curl e
muitas outras ferramentas de linha de comando. Isso nos permite enviar facil-
mente um nome de usuário e senha para a API, mas você precisa armazenar e va-
lidar essa senha com segurança. Um algoritmo de hash de senhaconverte cada se-
nha em uma string de aparência aleatória de comprimento fixo. Quando o usuá-
rio tenta fazer o login, a senha que ele apresenta é hash usando o mesmo algo-
ritmo e comparada com o hash armazenado no banco de dados. Isso permite que
a senha seja verificada sem armazená-la diretamente. Algoritmos modernos de
hash de senha, como Argon2, Scrypt, Bcrypt ouPBKDF2, são projetados para resis-
tir a uma variedade de ataques caso as senhas com hash sejam roubadas. Em par-
ticular, eles são projetados para levar muito tempo ou memória para serem pro-
cessados ​para evitar ataques de força brutapara recuperar as senhas. Você usará
o Scrypt neste capítulo, pois ele é seguro e amplamente implementado.
DEFINIÇÃO Um algoritmo de hash de senhaconverte senhas em valores de ta-
manho fixo de aparência aleatória, conhecidos como hash. Um hash de senha se-
gura usa muito tempo e memória para desacelerar ataques de força bruta, como
ataques de dicionário, em que um invasor tenta uma lista de senhas comuns para
ver se alguma corresponde ao hash.

Localize o arquivo pom.xml no projeto e abra-o com seu editor favorito. Adicione
a seguinte dependência do Scrypt à seção de dependências e salve oArquivo:

<dependência>
<groupId>com.lambdaworks</groupId>
<artifactId>criptografar</artifactId>
<version>1.4.0</version>
</dependência>

DICA Você mesmo pode evitar implementar o armazenamento de senhas usando


um protocolo LDAP (Protocolo leve de acesso a diretórios) diretório. Os servidores
LDAP geralmente implementam uma variedade de opções seguras de armazena-
mento de senhas. Você também pode terceirizar a autenticação para outra organi-
zação usando um protocolo de federaçãocomo SAML ou OpenID Connect. O Ope-
nID Connect é discutido no capítulo 7.

3.3.3 Criando o banco de dados de senhas

Antes davocê pode autenticar qualquer usuário, você precisa de alguma forma
para registrá-los. Por enquanto, você apenas permitirá que qualquer usuário se
registre fazendo uma solicitação POST para o /users terminal, especificando o
nome de usuário e a senha escolhida. Você adicionará esse endpoint na seção
3.3.4, mas primeiro vamos ver como armazenar senhas de usuários com segu-
rança no banco de dados.

DICA Em um projeto real, você pode confirmar a identidade do usuário durante


o registro (enviando um e-mail ou validando o cartão de crédito, por exemplo) ou
pode usar um repositório de usuário existente e não permitir que os usuários se
autorregistrem.

Você armazenará os usuários em uma nova tabela de banco de dados dedicada,


que precisará adicionar ao esquema do banco de dados. Abra o arquivo
schema.sql em src/main/resources em seu editor de texto, adicione a seguinte de-
finição de tabela na parte superior do arquivo e salve-o:

CREATE TABLE usuários(


user_id VARCHAR(30) PRIMARY KEY,
pw_hash VARCHAR(255) NÃO NULO
);

Você também precisa conceder as natter_api_user permissõespara ler e inse-


rir nesta tabela, então adicione a seguinte linha ao final do arquivo schema.sql e
salve-o novamente:

GRANT SELECT, INSERT ON users TO natter_api_user;

A tabela contém apenas o ID do usuário e seu hash de senha. Para armazenar um


novo usuário, você calcula o hash de sua senha e armazena na pw_hash coluna.
Neste exemplo, você usará a biblioteca Scrypt para hash da senha e, em seguida,
usará Dalesbred para inserir o valor hash no banco de dados.
O Scrypt usa vários parâmetros para ajustar a quantidade de tempo e memória
que usará. Você não precisa entender esses números, apenas saiba que números
maiores usarão mais tempo e memória da CPU. Você pode usar os parâmetros re-
comendados a partir de 2019 (consulte https://blog.filippo.io/the-scrypt-parame-
ters/ para uma discussão sobre os parâmetros do Scrypt), que deve levar cerca de
100ms em uma única CPU e 32MiB de memória:

String hash = SCryptUtil.scrypt(senha, 32768, 8, 1);

Isso pode parecer uma quantidade excessiva de tempo e memória, mas esses pa-
râmetros foram cuidadosamente escolhidos com base na velocidade com que os
invasores podem adivinhar as senhas. Máquinas dedicadas para quebrar senhas,
que podem ser construídas por quantias relativamente modestas de dinheiro, po-
dem tentar muitos milhões ou até bilhões de senhas por segundo. Os requisitos
caros de tempo e memória dos algoritmos de hash de senha segura, como o
Scrypt, reduzem isso para alguns milhares de senhas por segundo, aumentando
enormemente o custo para o invasor e dando aos usuários um tempo valioso para
alterar suas senhas após a descoberta de uma violação. A orientação mais recente
do NIST sobre armazenamento seguro de senhas (“verificadores secretos memori-
zados” na linguagem tortuosa do NIST) recomenda o uso de funções hash de me-
mória forte, como Scrypt (https://pages.nist.gov/800-63-3/sp800-
63b.html#memsecret ).

Se você tiver requisitos particularmente rígidos sobre o desempenho da autenti-


cação em seu sistema, poderá ajustar os parâmetros do Scrypt para reduzir o
tempo e os requisitos de memória para atender às suas necessidades. Mas você
deve procurar usar os padrões seguros recomendados até saber que eles estão
causando um impacto adverso no desempenho. Você deve considerar o uso de ou-
tros métodos de autenticação se o processamento de senha segura for muito caro
para seu aplicativo. Embora existam protocolos que permitem descarregar o
custo de hashing de senha para o cliente, como SCRAM 5 ou OPAQUE, 6 isso é difí-
cil de fazer com segurança, então você deve consultar um especialista antes de
implementar talsolução.

PRINCÍPIO Estabelecer padrões segurospara todos os algoritmos e parâmetros


sensíveis à segurança usados ​em sua API. Apenas relaxe os valores se não houver
outra maneira de atingir seus requisitos não relacionados à segurança.

3.3.4 Registrando usuários na API do Natter

Listagem 3.2mostra uma nova UserController classecom um método para re-


gistrar um usuário:

Primeiro, você lê o nome de usuário e a senha da entrada, certificando-se de va-


lidá-los conforme aprendido no capítulo 2.
Em seguida, você calcula um novo hash Scrypt da senha.
Por fim, armazene o nome de usuário e o hash juntos no banco de dados,
usando uma instrução preparada para evitar ataques de injeção de SQL.

Navegue até a pasta src/main/java/com/manning/apisecurityinaction/controller


em seu editor e crie um novo arquivo UserController.java. Copie o conteúdo da
listagem no editor e salve o novo arquivo.

Listagem 3.2 Registrando um novo usuário

pacote com.manning.apisecurityinaction.controller;

import com.lambdaworks.crypto.*;
import org.dalesbred.*;
import org.json.*;
importar faísca.*;

importar java.nio.charset.*;
importar java.util.*;

importar spark.Spark.* estático;

public class UserController {


string final estática privada USERNAME_PATTERN =
"[a-zA-Z][a-zA-Z0-9]{1,29}";

banco de dados de banco de dados final privado;

public UserController (banco de dados do banco de dados) {


this.database = banco de dados;
}

public JSONObject registerUser(Pedido de solicitação,


Resposta de resposta) lança Exception {
var json = new JSONObject(request.body());
var nome de usuário = json.getString("nome de usuário");
var senha = json.getString("senha");

if (!username.matches(USERNAME_PATTERN)) { ❶
throw new IllegalArgumentException("nome de usuário inválido");
}
if (senha.comprimento() < 8) {
lançar novo IllegalArgumentException(
"A senha deve conter pelo menos 8 caracteres");
}
var hash = SCryptUtil.scrypt(senha, 32768, 8, 1); ❷
database.updateUnique( ❸
"INSERT INTO users(user_id, pw_hash)" +
" VALUES(?, ?)", nome de usuário, hash);

resposta.status(201);
response.header("Localização", "/users/" + nome de usuário);
return new JSONObject().put("nome de usuário", nome de usuário);
}
}

❶ Aplique a mesma validação de nome de usuário que você usou antes.

❷ Use a biblioteca Scrypt para hash da senha. Use os parâmetros recomendados para
2019.

❸ Use uma instrução preparada para inserir o nome de usuário e o hash.

A biblioteca Scrypt gera um valor salt aleatório exclusivo para cada hash de se-
nha. A string de hash que é armazenada no banco de dados inclui os parâmetros
que foram usados ​quando o hash foi gerado, bem como esse valor de sal aleatório.
Isso garante que você sempre possa recriar o mesmo hash no futuro, mesmo que
altere os parâmetros. A biblioteca Scrypt poderá ler esse valor e decodificar os pa-
râmetros ao verificar o hash.

DEFINIÇÃO A salé um valor aleatório que é misturado à senha quando ela é


hash. Os sais garantem que o hash seja sempre diferente, mesmo que dois usuá-
rios tenham a mesma senha. Sem sais, um invasor pode criar um banco de dados
compactado de hashes de senha comuns, conhecido como tabela arco-íris, o que
permite que as senhas sejam recuperadas muito rapidamente.
Você pode adicionar uma nova rota para registrar um novo usuário em sua
Main classe. Localize o arquivo Main.java em seu editor e adicione as seguintes li-
nhas logo abaixo de onde você criou anteriormente o SpaceController objeto:

var userController = new UserController(banco de dados);


post("/users", userController::registerUser);

3.3.5 Autenticação de usuários

Paraautenticar um usuário, você extrairá o nome de usuário e a senha do cabeça-


lho de autenticação HTTP Basic, procurará o usuário correspondente no banco de
dados e, finalmente, verificará se a senha corresponde ao hash armazenado para
esse usuário. Nos bastidores, a biblioteca Scrypt extrairá o salt do hash de senha
armazenado, depois fará o hash da senha fornecida com o mesmo salt e parâme-
tros e, finalmente, comparará a senha com hash com o hash armazenado. Se fo-
rem iguais, o usuário deve ter apresentado a mesma senha e a autenticação é
bem-sucedida, caso contrário, falha.

A Listagem 3.3 implementa essa verificação como um filtro chamado antes de


cada chamada de API. Primeiro você verifica se existe um cabeçalho de autoriza-
ção na solicitação, com o esquema de autenticação básica. Então, se estiver pre-
sente, você pode extrair e decodificar as credenciais codificadas em Base64. Va-
lide o nome de usuário como sempre e procure o usuário no banco de dados. Por
fim, use a biblioteca Scrypt para verificar se a senha fornecida corresponde ao
hash armazenado para o usuário no banco de dados. Se a autenticação for bem-
sucedida, você deve armazenar o nome de usuário em um atributo na solicitação
para que outros manipuladores possam vê-lo; caso contrário, deixe-o nulo para
indicar um usuário não autenticado. Abra o arquivo UserController .java que você
criou anteriormente e inclua o método authenticate conforme fornecido na
listagem.

Listagem 3.3 Autenticando uma solicitação

public void authenticate(Solicitação de solicitação, Resposta de resposta) {


var authHeader = request.headers("Autorização"); ❶
if (authHeader == null || !authHeader.startsWith("Basic")) { ❶
return; ❶
} ❶

var offset = "Básico ".length();


var credenciais = new String(Base64.getDecoder().decode( ❷
authHeader.substring(offset)), StandardCharsets.UTF_8); ❷

var componentes = credenciais.split(":", 2); ❸


if (components.length != 2) { ❸
throw new IllegalArgumentException("cabeçalho de autenticação inválido"); ❸
} ❸

var nome de usuário = componentes[0]; ❸


var senha = componentes[1]; ❸

if (!username.matches(USERNAME_PATTERN)) {
throw new IllegalArgumentException("nome de usuário inválido");
}

var hash = database.findOptional(String.class,


"SELECT pw_hash FROM users WHERE user_id = ?", nome de usuário);

if (hash.isPresent() && ❹
SCryptUtil.check(password, hash.get())) { ❹
request.attribute("assunto", nome de usuário);
}
}

❶ Verifique se há um cabeçalho HTTP Basic Authorization.

❷ Decodifique as credenciais usando Base64 e UTF-8.

❸ Divida as credenciais em nome de usuário e senha.

❹ Se o usuário existir, use a biblioteca Scrypt para verificar a senha.

Você pode conectar isso na Main classecomo um filtro antes de todas as chama-
das de API. Abra o arquivo Main.java em seu editor de texto novamente e adici-
one a seguinte linha ao método main abaixo de onde você criou o userControl-
ler objeto:

before(userController::authenticate);

Agora você pode atualizar seus métodos de API para verificar se o usuário auten-
ticado corresponde a qualquer identidade reivindicada na solicitação. Por exem-
plo, você pode atualizar a operação Create Space para verificar se o owner cam-
pocorresponde ao usuário autenticado no momento. Isso também permite que
você pule a validação do nome de usuário, porque você pode confiar que o ser-
viço de autenticação já fez isso. Abra o arquivo SpaceController.java em seu edi-
tor e altere o createSpace métodopara verificar se o proprietário do espaço cor-
responde ao assunto autenticado, conforme o trecho a seguir:
public JSONObject createSpace(Solicitação de solicitação, Resposta de resposta) {
..
var proprietário = json.getString("proprietário");
var assunto = request.attribute("assunto");
if (!proprietário.igual(assunto)) {
lançar novo IllegalArgumentException(
"proprietário deve corresponder ao usuário autenticado");
}
..
}

Na verdade, você poderia remover o campo do proprietário da solicitação e sem-


pre usar o assunto do usuário autenticado, mas, por enquanto, você o deixará
como está. Você pode fazer o mesmo na operação Post Message no mesmo
arquivo:

var usuário = json.getString("autor");


if (!user.equals(request.attribute("assunto"))) {
lançar novo IllegalArgumentException(
"o autor deve corresponder ao usuário autenticado");
}

Agora você habilitou a autenticação para sua API - toda vez que um usuário faz
uma declaração sobre sua identidade, ele é obrigado a autenticar para fornecer
prova dessa declaração. Você ainda não está aplicando a autenticação em todas as
chamadas de API, portanto ainda pode ler as mensagens sem ser autenticado.
Você abordará isso em breve quando examinar o controle de acesso. As verifica-
ções que adicionamos até agora fazem parte da lógica do aplicativo. Agora vamos
experimentar como a API funciona. Primeiro, vamos tentar criar um espaçose-
mautenticando:

$ curl -d '{"name":"test space","owner":"demo"}'


➥ -H 'Content-Type: application/json' http://localhost:4567/spaces

{"error":"proprietário deve corresponder ao usuário autenticado"}

Bom, isso foi evitado. Vamos usar o curl agora para registrar um usuário de
demonstração:

$ curl -d '{"username":"demo","password":"password"}''
➥ -H 'Content-Type: application/json' http://localhost:4567/users

{"nome de usuário":"demonstração"}

Por fim, você pode repetir sua solicitação Create Space com as credenciais de au-
tenticação corretas:

$ curl -u demo:password -d '{"name":"test space","owner":"demo"}'


➥ -H 'Content-Type: application/json' http://localhost:4567/spaces

{"nome":"espaço de teste","uri":"/espaços/1"}

questionário

3. Quais das opções a seguir são propriedades desejáveis ​de um algoritmo de hash
de senha segura? (Pode haver várias respostas corretas.)
1. Deve ser fácil paralelizar.
2. Deve usar muito espaço de armazenamento em disco.
3. Deve usar muita largura de banda de rede.
4. Deve usar muita memória (vários MB).
5. Ele deve usar um sal aleatório para cada senha.
6. Ele deve usar muita energia da CPU para tentar muitas senhas.
4. Qual é a principal razão pela qual a autenticação HTTP Basic deve ser usada
apenas em um canal de comunicação criptografado, como HTTPS? (Escolha
uma resposta.)
1. A senha pode ser exposta no Referer cabeçalho.
2. O HTTPS retarda os invasores que tentam adivinhar senhas.
3. A senha pode ser adulterada durante a transmissão.
4. O Google penaliza os sites nas classificações de pesquisa se eles não usarem
HTTPS.
5. A senha pode ser facilmente decodificada por qualquer pessoa que bisbilhote
o tráfego da rede.

As respostas estão no final do capítulo.

3.4 Usando criptografia para manter os dados privados

Apresentandoa autenticação em sua API protege contra ameaças de falsificação.


No entanto, as solicitações para a API e as respostas dela não são protegidas de
forma alguma, levando a ameaças de adulteração e divulgação de informações.
Imagine que você está tentando verificar as últimas fofocas do seu grupo de tra-
balho enquanto está conectado a um ponto de acesso Wi-Fi público em sua cafete-
ria local. Sem criptografia, as mensagens enviadas de e para a API poderão ser li-
das por qualquer outra pessoa conectada ao mesmo ponto de acesso.
Seu esquema de autenticação de senha simples também é vulnerável a essa espio-
nagem, pois um invasor com acesso à rede pode simplesmente ler suas senhas co-
dificadas em Base64 conforme elas passam. Eles podem então se passar por qual-
quer usuário cuja senha tenham roubado. Muitas vezes, as ameaças estão ligadas
dessa maneira. Um invasor pode tirar proveito de uma ameaça, neste caso, a di-
vulgação de informações de comunicações não criptografadas e explorá-la para
fingir ser outra pessoa, minando a autenticação de sua API. Muitos ataques bem-
sucedidos no mundo real resultam do encadeamento de várias vulnerabilidades,
em vez de explorar apenas um erro.

Nesse caso, enviar senhas em texto não criptografado é uma vulnerabilidade


muito grande, então vamos corrigir isso ativando o HTTPS. HTTPS é HTTP normal,
mas a conexão ocorre por Transport Layer Security (TLS), que fornece criptogra-
fia e proteção de integridade. Depois de configurado corretamente, o TLS é ampla-
mente transparente para a API porque ocorre em um nível inferior na pilha de
protocolos e a API ainda vê solicitações e respostas normais. A Figura 3.5 mostra
como o HTTPS se encaixa na imagem, protegendo as conexões entre seus usuários
e a API.
Figura 3.5 A criptografia é usada para proteger dados em trânsito entre um cli-
ente e nossa API e em repouso quando armazenados no banco de dados.

Além de proteger os dados em trânsito (no caminho de e para nosso aplicativo),


você também deve considerar a proteção de quaisquer dados confidenciais em re-
pouso, quando armazenados no banco de dados do seu aplicativo. Muitas pessoas
diferentes podem ter acesso ao banco de dados, como parte legítima de seu traba-
lho ou devido ao acesso ilegítimo a ele por meio de alguma outra vulnerabilidade.
Por esse motivo, você também deve considerar a criptografia de dados privados
no banco de dados, conforme mostrado na figura 3.5. Neste capítulo, vamos nos
concentrar na proteção de dados em trânsito com HTTPS e discutir a criptografia
de dados no banco de dados no capítulo 5.

TLS ou SSL?
Segurança da Camada de Transporte (TLS) é um protocolo que se baseia no TCP/IP
e fornece várias funções básicas de segurança para permitir a comunicação se-
gura entre um cliente e um servidor. As primeiras versões do TLS eram conheci-
das como Secure Socket Layer, ou SSL, e muitas vezes você ainda ouvirá TLS refe-
rido como SSL. Os protocolos de aplicativos que usam TLS geralmente têm um S
anexado ao nome, por exemplo, HTTPS ou LDAPS, para significar “seguro”.

O TLS garante a confidencialidade e integridade dos dados transmitidos entre o


cliente e o servidor. Ele faz isso criptografando e autenticando todos os dados que
fluem entre as duas partes. Na primeira vez que um cliente se conecta a um servi-
dor, é realizado um handshake TLS no qual o servidor se autentica no cliente,
para garantir que o cliente se conectou ao servidor ao qual deseja se conectar (e
não a um servidor sob o controle de um invasor). Em seguida, novas chaves crip-
tográficas são negociadas para esta sessão e usadas para criptografar e autenticar
todas as solicitações e respostas a partir de então. Você examinará detalhada-
mente TLS e HTTPS no capítulo 7.
3.4.1 Habilitando HTTPS

PossibilitandoO suporte a HTTPS no Spark é direto. Primeiro, você precisa gerar


um certificadoque a API usará para se autenticar para seus clientes. Os certifica-
dos TLS são abordados em detalhes no capítulo 7. Quando um cliente se conecta à
sua API, ele usa um URI que inclui o nome do host do servidor em que a API está
sendo executada, por exemplo api .example.com . O servidor deve apresentar
um certificado, assinado por uma autoridade de certificação confiável(CA), que
diz que realmente é o servidor para api.example.com . Se um certificado invá-
lido for apresentado ou não corresponder ao host ao qual o cliente deseja se co-
nectar, o cliente interromperá a conexão. Sem essa etapa, o cliente pode ser indu-
zido a se conectar ao servidor errado e enviar sua senha ou outros dados confi-
denciais ao impostor.

Como você está habilitando o HTTPS apenas para fins de desenvolvimento, pode
usar um certificado autoassinado. Em capítulos posteriores, você se conectará à
API diretamente em um navegador da Web, portanto, é muito mais fácil usar um
certificado assinado por uma CA local. A maioria dos navegadores da Web não
gosta de certificados autoassinados. Uma ferramenta chamada mkcert (
https://mkcert.dev ) simplifica consideravelmente o processo. Siga as instruções
na página inicial do mkcert para instalá-lo e, em seguida, execute

mkcert -install

para gerar o certificado CA e instalá-lo. O certificado CA será marcado automatica-


mente como confiável pelos navegadores da Web instalados em seu sistema
operacional.

DEFINIÇÃO Um certificado autoassinadoé um certificado que foi assinado


usando a chave privada associada a esse mesmo certificado, em vez de uma auto-
ridade de certificação confiável. Os certificados autoassinados devem ser usados ​
somente quando você tiver uma relação de confiança direta com o proprietário
do certificado, como quando você mesmo gerou o certificado.

Agora você pode gerar um certificado para seu servidor Spark em execução no lo-
calhost. Por padrão, mkcert gera certificados no Privacy Enhanced Mail(PEM).
Para Java, você precisa do certificado no formato PKCS#12, então execute o se-
guinte comando na pasta raiz do projeto Natter para gerar um certificado para
localhost:
mkcert -pkcs12 host local

O certificado e a chave privada serão gerados em um arquivo chamado


localhost.p12 . Por padrão, a senha desse arquivo é changeit . Agora você
pode ativar o suporte HTTPS no Spark adicionando uma chamada
ao secure () método estático, conforme mostrado na Listagem 3.4. Os dois pri-
meiros argumentos do método fornecem o nome do arquivo keystore que contém
o certificado do servidor e a chave privada. Deixe os argumentos restantes como
null ; eles são necessários apenas se você quiser oferecer suporte à autenticação
de certificado de cliente (que é abordada no capítulo 11).

AVISO O certificado CA e a chave privada que mkcert gera pode ser usado para
gerar certificados para qualquer site que seja confiável para o seu navegador. Não
compartilhe esses arquivos nem os envie a ninguém. Quando terminar o desen-
volvimento, considere a execução mkcert -uninstall para remover a CA dos
armazenamentos confiáveis ​do sistema.

Listagem 3.4 Habilitando HTTPS

importar spark.Spark.secure estático; ❶

public class Principal {


public static void main(String... args) lança Exception {
secure("localhost.p12", "changeit", nulo, nulo); ❷
..
}
}

❶ Importe o método seguro.


❷ Ative o suporte HTTPS no início do método principal.

Reinicie o servidor para que as alterações entrem em vigor. Se você iniciou o ser-
vidor a partir da linha de comando, pode usar Ctrl-C para interromper o processo
e simplesmente executá-lo novamente. Se você iniciou o servidor a partir do seu
IDE, deve haver um botão para reiniciar o processo.

Por fim, você pode chamar sua API (depois de reiniciar o servidor). Se o curl se re-
cusar a conectar, você pode usar a --cacert opçãoenrolar para dizer a ele para
confiar no mkcertcertificado:

$ curl --cacert "$(mkcert -CAROOT)/rootCA.pem"


➥ -d '{"username":"demo","password":"password"}'
➥ -H 'Content-Type: application/json ' https://localhost:4567/users

{"nome de usuário":"demonstração"}

AVISO Não fique tentado a desabilitar a validação do certificado TLS passando


o -k ou --insecure opçõespara enrolar (ou opções semelhantes em uma biblio-
teca HTTPS). Embora isso possa ser aceitável em um ambiente de desenvolvi-
mento, desabilitar a validação do certificado em um ambiente de produção preju-
dica as garantias de segurança do TLS. Adquira o hábito de gerar e usar certifica-
dos corretos. Não é muito mais difícil e é menos provável que você cometa erros
mais tarde.

3.4.2 Segurança de transporte estrita

Quandoum usuário visita um site em um navegador, o navegador primeiro ten-


tará se conectar à versão HTTP não segura de uma página, pois muitos sites ainda
não oferecem suporte a HTTPS. Um site seguro redirecionará o navegador para a
versão HTTPS da página. Para uma API, você só deve expor a API por HTTPS por-
que os usuários não se conectarão diretamente aos pontos de extremidade da API
usando um navegador da Web e, portanto, você não precisa oferecer suporte a
esse comportamento herdado. Os clientes da API também costumam enviar dados
confidenciais, como senhas, na primeira solicitação, portanto, é melhor rejeitar
completamente as solicitações não HTTPS. Se, por algum motivo, você precisar
oferecer suporte a navegadores da Web que se conectam diretamente aos seus
terminais de API, a melhor prática é redirecioná-los imediatamente para a versão
HTTPS da API e definir o HTTP Strict-Transport-Security(HSTS) para instruir o na-
vegador a sempre usar a versão HTTPS no futuro. Se você adicionar a linha a se-
guir ao afterAfter filtro em seu método principal, ele adicionará um cabeçalho
HSTS atudorespostas:

response.header("Strict-Transport-Security", "max-age=31536000");

DICA Adicionar um cabeçalho HSTS para localhost não é uma boa ideia, pois
impedirá que você execute servidores de desenvolvimento em HTTP simples até
que o max-age atributoexpira. Se você quiser experimentar, defina um max-
age valor curto.

questionário

5. Lembrando a tríade CIA do capítulo 1, qual das seguintes metas de segurança


não é fornecida pelo TLS?
1. Confidencialidade
2. Integridade
3. Disponibilidade

A resposta está no final do capítulo.


3.5 Log de auditoria para responsabilidade

Responsabilidade rconsiste em ser capaz de determinar quem fez o quê e quando.


A maneira mais simples de fazer isso é manter um registro das ações que as pes-
soas executam usando sua API, conhecido como registro de auditoria. A Figura 3.6
repete o modelo mental que você deve ter para os mecanismos discutidos neste
capítulo. O registro de auditoria deve ocorrer após a autenticação, para que você
saiba quem está executando uma ação, mas antes de tomar decisões de autoriza-
ção que possam negar o acesso. A razão para isso é que você deseja registrar todas
as tentativas de operação, não apenas as bem-sucedidas. Tentativas malsucedidas
de executar ações podem ser indícios de uma tentativa de ataque. É difícil exage-
rar a importância de um bom log de auditoria para a segurança de uma API. Os
logs de auditoria devem ser gravados em um armazenamento durável, como o sis-
tema de arquivos ou um banco de dados,
Figura 3.6 O log de auditoria deve ocorrer tanto antes de uma solicitação ser pro-
cessada quanto após sua conclusão. Quando implementado como um filtro, deve
ser colocado após a autenticação, para que você saiba quem está realizando cada
ação, mas antes das verificações de controle de acesso para que você registre as
operações que foram tentadas, mas negadas.

Felizmente, dada a importância do log de auditoria, é fácil adicionar alguns recur-


sos básicos de log à sua API. Nesse caso, você fará login em uma tabela de banco
de dados para poder visualizar e pesquisar facilmente os logs da própria API.

DICA Em um ambiente de produção, você normalmente desejará enviar logs de


auditoria para uma ferramenta centralizada de coleta e análise de logs, conhecida
como SIEM (Security Information and Event Management), para que possam ser
correlacionados com logs de outros sistemas e analisados ​quanto a possíveis ame-
aças e comportamentos incomuns.
Quanto à nova funcionalidade anterior, você adicionará uma nova tabela de
banco de dados para armazenar os logs de auditoria. Cada entrada terá um identi-
ficador (usado para correlacionar os logs de solicitação e resposta), juntamente
com alguns detalhes da solicitação e da resposta. Adicione a seguinte definição de
tabela a schema.sql.

OBSERVAÇÃO A tabela de auditoria não deve ter nenhuma restrição de refe-


rência para nenhuma outra tabela. Os logs de auditoria devem ser registrados
com base na solicitação, mesmo que os detalhes sejam inconsistentes com outros
dados.

CREATE TABLE audit_log(


audit_id INT NULO,
método VARCHAR(10) NOT NULL,
caminho VARCHAR(100) NÃO NULO,
user_id VARCHAR(30) NULL,
estado INT NULO,
audit_time TIMESTAMP NÃO NULO
);
CREATE SEQUENCE audit_id_seq;

Como antes, você também precisa conceder as permissões apropriadas


ao natter_api_user , portanto, no mesmo arquivo, adicione a seguinte linha ao
final do arquivo e salve:

GRANT SELECT, INSERT ON audit_log TO natter_api_user;

Um novo controlador agora pode ser adicionado para lidar com o log de audito-
ria. Você divide o registro em dois filtros, um que ocorre antes do processamento
da solicitação (após a autenticação) e outro que ocorre depois que a resposta foi
produzida. Você também permitirá o acesso aos logs a qualquer pessoa para fins
de ilustração. Normalmente, você deve bloquear os logs de auditoria apenas para
um pequeno número de usuários confiáveis, pois eles geralmente são confidenci-
ais. Freqüentemente, os usuários que podem acessar os logs de auditoria (audito-
res) são diferentes dos administradores de sistema normais, pois as contas de ad-
ministrador são as mais privilegiadas e, portanto, as que mais precisam de moni-
toramento. Este é um importante princípio de segurança conhecido como separa-
ção de funções.

DEFINIÇÃO O princípio da separação de funçõesexige que diferentes aspectos


de ações privilegiadas sejam controlados por pessoas diferentes, de modo que ne-
nhuma pessoa seja a única responsável pela ação. Por exemplo, um administra-
dor de sistema também não deve ser responsável por gerenciar os logs de audito-
ria desse sistema. Nos sistemas financeiros, a separação de funções é
freqüentemente usada para garantir que a pessoa que solicita um pagamento não
seja a mesma pessoa que aprova o pagamento, fornecendo um cheque contra
fraude.

Em seu editor, navegue até


src/main/java/com/manning/apisecurityinaction/controller e crie um novo ar-
quivo chamado AuditController.java. A Listagem 3.5 mostra o conteúdo desse
novo controlador que você deve copiar no arquivo e salvar. Conforme mencio-
nado, o registro é dividido em dois filtros: um que é executado antes de cada ope-
ração e outro que é executado depois. Isso garante que, se o processo travar du-
rante o processamento de uma solicitação, você ainda poderá ver quais solicita-
ções estavam sendo processadas no momento. Se você registrasse apenas as res-
postas, perderia qualquer traço de uma solicitação se o processo travasse, o que
seria um problema se um invasor encontrasse uma solicitação que causou a falha.
Para permitir que alguém analise os logs para correlacionar solicitações com res-
postas, gere um ID de log de auditoria exclusivo no auditRequestStart méto-
doe adicione-o como um atributo à solicitação. No
auditRequestEnd método,você pode recuperar o mesmo ID de log de auditoria
para que os dois eventos de log possam ser vinculados.

Listagem 3.5 O controlador de log de auditoria

pacote com.manning.apisecurityinaction.controller;

import org.dalesbred.*;
import org.json.*;
importar faísca.*;

importar java.sql.*;
importar java.time.*;
importar java.time.temporal.*;

public class AuditController {

banco de dados de banco de dados final privado;

public AuditController(banco de dados do banco de dados) {


this.database = banco de dados;
}

public void auditRequestStart(Pedido de solicitação, Resposta de resposta) {


database.withVoidTransaction(tx -> {
var auditId = database.findUniqueLong( ❶
"SELECIONE O PRÓXIMO VALOR PARA audit_id_seq"); ❶
request.attribute("audit_id", auditId); ❶
database.updateUnique(
"INSERT INTO audit_log(audit_id, método, caminho, " +
"user_id, audit_time) " +
"VALUES(?, ?, ?, ?, current_timestamp)",
auditId,
request.requestMethod(),
request.pathInfo(),
request.attribute("assunto"));
});
}

public void auditRequestEnd(Pedido de solicitação, Resposta de resposta) {


database.updateUnique(
"INSERT INTO audit_log(audit_id, método, caminho, status, " +
"user_id, audit_time) " +
"VALUES(?, ?, ?, ?, ?, current_timestamp)",
request.attribute("audit_id"), ❷
request.requestMethod(),
request.pathInfo(),
resposta.status(),
request.attribute("assunto"));
}
}

❶ Gere um novo ID de auditoria antes que a solicitação seja processada e salve-o


como um atributo na solicitação.

❷ Ao processar a resposta, procure o id de auditoria nos atributos da solicitação.

A Listagem 3.6 mostra o código para ler as entradas do log de auditoria da última
hora. As entradas são consultadas no banco de dados e convertidas em objetos
JSON usando um RowMapper método personalizado. A lista de registros é retor-
nada como uma matriz JSON. Um limite simples é adicionado à consulta para evi-
tar que muitos resultados sejam retornados.

Listagem 3.6 Lendo entradas de log de auditoria

public JSONArray readAuditLog(Solicitação de solicitação, Resposta de resposta) {


var since = Instant.now().minus(1, ChronoUnit.HOURS); ❶
var logs = database.findAll(AuditController::recordToJson, ❶
"SELECT * FROM audit_log " + ❶
"WHERE audit_time >= ? LIMIT 20", since); ❶
retornar novo JSONArray(logs); ❷
}

private static JSONObject recordToJson(ResultSet row) ❸


throws SQLException { ❸
return new JSONObject() ❸
.put("id", row.getLong("audit_id")) ❸
.put("method", row.getString("method" )) ❸
.put("caminho", linha.getString("caminho")) ❸
.put("status", linha.getInt("status")) ❸
.put("usuário", linha.getString("user_id ")) ❸
.put("time", row.getTimestamp("audit_time").toInstant()); ❸
} ❸

❶ Leia as entradas de log da última hora.

❷ Converta cada entrada em um objeto JSON e colete como uma matriz JSON.

❸ Use um método auxiliar para converter os registros em JSON.


Podemos então conectar esse novo controlador ao seu método principal, tendo o
cuidado de inserir o filtro entre o filtro de autenticação e os filtros de controle de
acesso para operações individuais. Como os filtros do Spark devem ser executados
antes ou depois (e não ao redor) de uma chamada de API, você define filtros sepa-
rados para serem executados antes e depois de cada solicitação.

Abra o arquivo Main.java em seu editor e localize as linhas que instalam os filtros
para autenticação. O log de auditoria deve vir logo após a autenticação, então
você deve adicionar os filtros de auditoria entre o filtro de autenticação e a pri-
meira definição de rota, conforme destacado em negrito neste próximo trecho.
Adicione as linhas indicadas e salve o arquivo.

before(userController::authenticate);

var auditController = new AuditController(banco de dados); ❶


before(auditController::auditRequestStart); ❶
afterAfter(auditController::auditRequestEnd); ❶

post("/espaços",
spaceController::createSpace);

❶ Adicione estas linhas para criar e registrar o controlador de auditoria.

Por fim, você pode registrar um novo endpoint (não seguro) para ler os logs. No-
vamente, em um ambiente de produção, isso deve ser desativado ou bloqueado:

get("/logs", auditController::readAuditLog);
Uma vez instalado e o servidor reiniciado, faça algumas solicitações de amostra e
visualize o log de auditoria. Você pode usar o utilitário jq (
https://stedolan.github.io/jq/ ) para imprimir a saída:

$ curl pem https://localhost:4567/logs | jq


[
{
"caminho": "/usuários",
"método": "POST",
"id": 1,
"time": "2019-02-06T17:22:44.123Z"
},
{
"caminho": "/usuários",
"método": "POST",
"id": 1,
"time": "2019-02-06T17:22:44.237Z",
"estado": 201
},
{
"caminho": "/espaços/1/mensagens/1",
"método": "EXCLUIR",
"id": 2,
"time": "2019-02-06T17:22:55.266Z",
"usuário": "demonstração"
},...
]

Este estilo de log é um log de acesso básico, que registra as solicitações e respostas
HTTP brutas para sua API. Outra maneira de criar um log de auditoria é capturar
eventos na camada de lógica de negócios de seu aplicativo, como eventos criados
pelo usuário ou mensagens postadas. Esses eventos descrevem os detalhes essen-
ciais do que aconteceu sem referência ao protocolo específico usado para acessar
a API. Ainda outra abordagem é capturar eventos de auditoria diretamente no
banco de dados usando gatilhos para detectar quando os dados são alterados. A
vantagem dessas abordagens alternativas é que elas garantem que os eventos se-
jam registrados independentemente de como a API é acessada, por exemplo, se a
mesma API estiver disponível por HTTP ou usando um protocolo RPC binário. A
desvantagem é que alguns detalhes são perdidos e alguns ataques em potencial
podem ser perdidos devido a essa faltadetalhe.

questionário

6. Qual princípio de design seguro indicaria que os logs de auditoria devem ser ge-
renciados por usuários diferentes dos administradores de sistema normais?
1. O princípio de Pedro
2. O princípio do menor privilégio
3. O princípio da defesa em profundidade
4. O princípio da separação de funções
5. O princípio da segurança através da obscuridade

A resposta está no final do capítulo.

3.6 Controle de acesso

Vocêagora tem um mecanismo de autenticação baseado em senha razoavelmente


seguro, juntamente com HTTPS para proteger dados e senhas na transmissão en-
tre o cliente e o servidor da API. No entanto, você ainda permite que qualquer
usuário execute qualquer ação. Qualquer usuário pode postar uma mensagem em
qualquer espaço social e ler todas as mensagens desse espaço. Qualquer usuário
também pode decidir ser moderador e excluir mensagens de outros usuários.
Para corrigir isso, agora você implementará verificações básicas de controle de
acesso.

O controle de acesso deve acontecer após a autenticação, para que você saiba
quem está tentando realizar a ação, conforme a figura 3.7. Se a solicitação for con-
cedida, ela poderá prosseguir para a lógica do aplicativo. No entanto, se for ne-
gado pelas regras de controle de acesso, ele deve ser reprovado imediatamente e
uma resposta de erro retornada ao usuário. Os dois principais códigos de status
HTTP para indicar que o acesso foi negado são 401 Não autorizado e 403 Proibido.
Consulte a barra lateral para obter detalhes sobre o significado desses dois códi-
gos e quando usar um ou outro.

Figura 3.7 O controle de acesso ocorre após a autenticação e a solicitação foi regis-
trada para auditoria. Se o acesso for negado, uma resposta proibida será retor-
nada imediatamente sem executar nenhuma lógica do aplicativo. Se o acesso for
concedido, a solicitação prosseguirá normalmente.

Códigos de status HTTP 401 e 403

O HTTP inclui dois códigos de status padrão para indicar que o cliente falhou nas
verificações de segurança e pode ser confuso saber qual status usar em quais
situações.

O 401 Unauthorized código de status, apesar do nome, indica que o servidor


exigiu autenticação para esta solicitação, mas o cliente não forneceu nenhuma
credencial, estava incorreta ou era do tipo errado. O servidor não sabe se o usuá-
rio está autorizado ou não porque não sabe quem é. O cliente (ou usuário) pode
corrigir a situação tentando credenciais diferentes. Um WWW-Authenticate cabe-
çalho padrão pode ser retornado para informar ao cliente quais credenciais ele
precisa, que serão retornadas no Authorization cabeçalho. Confuso ainda? In-
felizmente, as especificações HTTP usam as palavras autorização e autenticação
como se fossem idênticas.

O 403 Forbidden código de status, por outro lado, informa ao cliente que suas
credenciais foram válidas para autenticação, mas que não tem permissão para re-
alizar a operação solicitada. Isso é uma falha de autorização, não de autentica-
ção. Normalmente, o cliente não pode fazer nada sobre isso, a não ser solicitar
acesso ao administrador.

3.6.1 Aplicação da autenticação

oa verificação de controle de acesso mais básica é simplesmente exigir que todos


os usuários sejam autenticados. Isso garante que apenas usuários genuínos da API
possam obter acesso, sem impor nenhum outro requisito. Você pode impor isso
com um filtro simples que é executado após a autenticação e verifica se um as-
sunto genuíno foi registrado nos atributos da solicitação. Se nenhum atributo de
assunto for encontrado, ele rejeita a solicitação com um código de status 401 e
adiciona um WWW-Authenticate cabeçalho padrãopara informar ao cliente que
o usuário deve autenticar com autenticação básica. Abra o arquivo
UserController.java em seu editor e adicione o seguinte método, que pode ser
usado como um before filtro Sparkpara garantir que os usuários sejam
autenticados:

public void requireAuthentication(Request request,


Resposta resposta) {
if (request.attribute("assunto") == nulo) {
response.header("WWW-Authenticate",
"Reino básico=\"/\", charset=\"UTF-8\"");
parada(401);
}
}

Em seguida, você pode abrir o arquivo Main.java e exigir que todas as chamadas
para a API do Spaces sejam autenticadas, adicionando a seguinte definição de fil-
tro. Conforme mostrado na figura 3.7 e ao longo deste capítulo, verificações de
controle de acesso como esta devem ser adicionadas após autenticação e registro
de auditoria. Localize a linha onde você adicionou o filtro de autenticação anteri-
ormente e adicione um filtro para aplicar a autenticação em todas as solicitações
à API que começam com o caminho de URL /spaces, para que o código se pareça
com o seguinte:

before(userController::authenticate); ❶
before(auditController::auditRequestStart); ❷
afterAfter(auditController::auditRequestEnd); ❷
before("/spaces", userController::requireAuthentication); ❸
post("/spaces", spaceController::createSpace); ..

❶ Primeiro, tente autenticar o usuário.

❷ Em seguida, execute o log de auditoria.

❸ Finalmente, adicione a verificação se a autenticação foi bem-sucedida.

Se você salvar o arquivo e reiniciar o servidor, agora poderá ver as solicitações


não autenticadas para criar um espaço serem rejeitadas com um erro 401 solici-
tando autenticação, como no exemplo a seguirexemplo:

$ curl -i -d '{"name":"test space","owner":"demo"}'


➥ -H 'Content-Type: application/json' https://localhost:4567/spaces
HTTP/1.1 401 não autorizado
Data: segunda-feira, 18 de março de 2019 14:51:40 GMT
WWW-Authenticate: Basic realm="/", charset="UTF-8"
...

Repetir a solicitação com credenciais de autenticação permite que ela seja bem-
sucedida:

$ curl -i -d '{"name":"test space","owner":"demo"}'


➥ -H 'Content-Type: application/json' -u demo:changeit
➥ https://localhost: 4567/espaços
HTTP/1.1 201 Criado
...
{"nome":"espaço de teste","uri":"/espaços/1"}

3.6.2 Listas de controle de acesso

Alémexigindo simplesmente que os usuários sejam autenticados, você também


pode querer impor restrições adicionais sobre quem pode executar determinadas
operações. Nesta seção, você implementará um método de controle de acesso
muito simples com base no fato de um usuário ser membro do espaço social que
está tentando acessar. Você conseguirá isso controlando quais usuários são mem-
bros de quais espaços sociais em uma estrutura conhecida como lista de controle
de acesso (ACL).

Cada entrada para um espaço listará um usuário que pode acessar esse espaço,
juntamente com um conjunto de permissõesque definem o que podem fazer. A
API do Natter tem três permissões: ler mensagens em um espaço, postar mensa-
gens nesse espaço e uma permissão de exclusão concedida aos moderadores.

DEFINIÇÃO Uma lista de controle de acesso é uma lista de usuários que podem
acessar um determinado objeto, juntamente com um conjunto de permissõesque
definem o que cada usuário pode fazer.

Por que simplesmente não permitir que todos os usuários autenticados executem
qualquer operação? Em algumas APIs, esse pode ser um modelo de segurança
apropriado, mas para a maioria das APIs algumas operações são mais confidenci-
ais do que outras. Por exemplo, você pode permitir que qualquer pessoa em sua
empresa veja suas próprias informações salariais em sua API de folha de paga-
mento, mas a capacidade de alterar o salário de alguém normalmente não é algo
que você permitiria que qualquer funcionário fizesse! Lembre-se do princípio da
menor autoridade(POLA) do capítulo 1, que diz que qualquer usuário (ou pro-
cesso) deve receber exatamente a quantidade certa de autoridade para fazer os
trabalhos que precisa fazer. Muitas permissões podem causar danos ao sistema.
Muito poucas permissões e eles podem tentar contornar a segurança do sistema
para fazer seu trabalho.

As permissões serão concedidas aos usuários em uma nova permissions tabela,


que vincula um usuário a um conjunto de permissões em um determinado espaço
social. Para simplificar, você representará as permissões como uma string dos ca-
racteres r (ler), w (escrever) e d (excluir). Adicione a seguinte definição de tabela
ao final de schema.sql em seu editor de texto e salve a nova definição. Deve vir
depois do spaces e users definições de tabela à medida que as referencia para
garantir que as permissões possam ser concedidas apenas para espaços existentes
e usuários reais.

Permissões CREATE TABLE(


space_id INT NOT NULL REFERENCES spaces(space_id),
user_id VARCHAR(30) NÃO NULL REFERENCES users(user_id),
perm VARCHAR(3) NÃO NULO,
CHAVE PRIMÁRIA (space_id, user_id)
);
GRANT SELECT, INSERT ON permissões TO natter_api_user;

Em seguida, você precisa garantir que o proprietário inicial de um espaço receba


todas as permissões. Você pode atualizar o createSpace métodopara conceder
todas as permissões ao proprietário na mesma transação que criamos o espaço.
Abra SpaceController.java em seu editor de texto e localize o createSpace mé-
todo. Adicione as linhas destacadas na listagem a seguir:
return database.withTransaction(tx -> {
var spaceId = database.findUniqueLong(
"SELECIONE O PRÓXIMO VALOR PARA space_id_seq;");

database.updateUnique(
"INSERT INTO espaços(space_id, nome, proprietário)" +
"VALUES(?, ?, ?);", spaceId, spaceName, proprietário);

database.updateUnique( ❶
"INSERT INTO permissions(space_id, user_id, perms) " + ❶
"VALUES(?, ?, ?)", spaceId, owner, "rwd"); ❶

resposta.status(201);
response.header("Localização", "/espaços/" + spaceId);

retornar novo JSONObject()


.put("nome", espaçoNome)
.put("uri", "/espaços/" + spaceId);
});

❶ Certifique-se de que o proprietário do espaço tenha todas as permissões no espaço


recém-criado.

Agora você precisa adicionar verificações para garantir que o usuário tenha as
permissões apropriadas para as ações que está tentando executar. Você pode codi-
ficar essas verificações em cada método individual, mas é muito mais fácil man-
ter as decisões de controle de acesso usando filtros que são executados antes
mesmo de o controlador ser chamado. Essa separação de preocupações garante
que o controlador possa se concentrar na lógica principal da operação, sem ter
que se preocupar com os detalhes do controle de acesso. Isso também garante
que, se você quiser alterar a forma como o controle de acesso é executado, poderá
fazer isso no filtro comum, em vez de alterar cada método do controlador.

OBSERVAÇÃO As verificações de controle de acesso geralmente são incluídas


diretamente na lógica de negócios, porque quem tem acesso ao que é, em última
análise, uma decisão de negócios. Isso também garante que as regras de controle
de acesso sejam aplicadas de forma consistente, independentemente de como essa
funcionalidade é acessada. Por outro lado, separar as verificações de controle de
acesso facilita a centralização do gerenciamento de políticas, como você verá no
capítulo 8.

Para impor suas regras de controle de acesso, você precisa de um filtro que possa
determinar se o usuário autenticado tem as permissões apropriadas para execu-
tar uma determinada operação em um determinado espaço. Em vez de ter um fil-
tro que tenta determinar qual operação está sendo executada examinando a soli-
citação, você escreverá um método de fábrica que retorna um novo filtro com de-
talhes sobre a operação. Você pode usar isso para criar filtros específicos para
cada operação. A Listagem 3.7 mostra como implementar esse filtro em sua
UserController classe.

Abra UserController.java e adicione o método da listagem 3.7 à classe abaixo dos


outros métodos existentes. O método recebe como entrada o nome do método
HTTP que está sendo executado e a permissão necessária. Se o método HTTP não
corresponder, ignore a validação dessa operação e deixe que outros filtros lidem
com isso. Antes de impor quaisquer regras de controle de acesso, você deve pri-
meiro garantir que o usuário seja autenticado, portanto, adicione uma chamada
ao requireAuthentication filtro existente. Em seguida, você pode procurar o
usuário autenticado no banco de dados do usuário e determinar se ele tem as per-
missões necessárias para executar esta ação, neste caso, por uma simples corres-
pondência de string com as letras de permissão. Para casos mais complexos, con-
vém converter as permissões em um Set objeto e verificar explicitamente se to-
das as permissões necessárias estão contidas no conjunto de permissões do
usuário.

DICA EnumSet A classe Javapode ser usado para representar eficientemente um


conjunto de permissões como um vetor de bits, fornecendo uma maneira com-
pacta e rápida de verificar rapidamente se um usuário possui um conjunto de
permissões necessárias.

Se o usuário não tiver as permissões necessárias, você deverá reprovar a solicita-


ção com um código de status 403 Proibido. Isso informa ao usuário que ele não
tem permissão para executar a operação que estásolicitando.

Listagem 3.7 Verificando permissões em um filtro

public Filter requirePermission(método String, permissão String) {


return (solicitação, resposta) -> { ❶
if (!method.equalsIgnoreCase(request.requestMethod())) { ❷
return; ❷
} ❷

requireAuthentication(solicitação, resposta); ❸

var spaceId = Long.parseLong(request.params(":spaceId"));


var nome de usuário = (String) request.attribute("assunto");

var perms = database.findOptional(String.class, ❹


"SELECT perms FROM permissions " + ❹
"WHERE space_id = ? AND user_id = ?", ❹
spaceId, username).orElse(""); ❹

if (!perms.contains(permission)) { ❺
halt(403); ❺
} ❺
};
}

❶ Retorne um novo filtro Spark como uma expressão lambda.

❷ Ignore solicitações que não correspondam ao método de solicitação.

❸ Primeiro verifique se o usuário está autenticado.

❹ Pesquise as permissões do usuário atual no espaço fornecido, assumindo como pa-


drão nenhuma permissão.

❺ Se o usuário não tiver permissão, interrompa com o status 403 Proibido.

3.6.3 Aplicação do controle de acesso no Natter

Vocêagora você pode adicionar filtros a cada operação em seu método principal,
conforme mostrado na Listagem 3.8. Antes de cada rota do Spark, você adiciona
uma nova before () filtro que aplica as permissões corretas. Cada caminho de
filtro deve ter um :spaceId parâmetro de caminhopara que o filtro possa deter-
minar em qual espaço está sendo operado. Abra a classe Main.java em seu editor
e certifique-se de que seu main() métodocorresponde ao conteúdo da listagem
3.8. Novos filtros que impõem verificações de permissão são destacados em
negrito.
OBSERVAÇÃO As implementações de todas as operações de API podem ser en-
contradas no repositório GitHub que acompanha o livro em
https://github.com/NeilMadden/apisecurityinaction .

Listagem 3.8 Adicionando filtros de autorização

public static void main(String... args) lança Exception {


...
before(userController::authenticate); ❶

before(auditController::auditRequestStart);
afterAfter(auditController::auditRequestEnd);

before("/spaces", ❷
userController::requireAuthentication); ❷
post("/espaços",
spaceController::createSpace);

before("/espaços/:espaçoId/mensagens", ❸
userController.requirePermission("POST", "w"));
post("/espaços/:espaçoId/mensagens",
spaceController::postMessage);

before("/espaços/:espaçoId/mensagens/*",
userController.requirePermission("GET", "r"));
get("/espaços/:espaçoId/messages/:msgId",
spaceController::readMessage);

before("/espaços/:espaçoId/mensagens",
userController.requirePermission("GET", "r"));
get("/espaços/:espaçoId/mensagens",
spaceController::findMessages);

var moderadorController =
novo ModeradorController(banco de dados);

before("/espaços/:espaçoId/mensagens/*",
userController.requirePermission("DELETE", "d"));
delete("/espaços/:spaceId/messages/:msgId",
moderadorController::deletePost);

post("/users", userController::registerUser); ❹

...
}

❶ Antes de mais nada, você deve tentar autenticar o usuário.

❷ Qualquer pessoa pode criar um espaço, então você apenas impõe que o usuário es-
teja logado.

❸ Para cada operação, você adiciona um filtro before() que garante que o usuário te-
nha as permissões corretas.

❹ Qualquer pessoa pode registrar uma conta e não será autenticada primeiro.

Com isso em vigor, se você criar um segundo usuário “demo2” e tentar ler uma
mensagem criada pelo usuário de demonstração existente em seu espaço, rece-
berá um erro 403 Proibidoresposta:

$ curl -i -u demo2:password
➥ https://localhost:4567/spaces/1/messages/1
HTTP/1.1 403 Proibido
...

3.6.4 Adicionando novos membros a um espaço Natter

Entãoaté agora, não há como nenhum usuário que não seja o proprietário do es-
paço postar ou ler mensagens de um espaço. Vai ser uma rede social bastante an-
tissocial, a menos que você possa adicionar outros usuários! Você pode adicionar
uma nova operação que permite que outro usuário seja adicionado a um espaço
por qualquer usuário existente que tenha permissão de leitura nesse espaço. A
próxima listagem adiciona uma operação ao SpaceController para permitir
isso.

Abra SpaceController.java em seu editor e adicione o addMember métododa lista-


gem 3.9 para a classe. Primeiro, valide se as permissões fornecidas correspondem
ao rwd formulário que você está usando. Você pode fazer isso usando uma ex-
pressão regular. Nesse caso, insira as permissões desse usuário na permissi-
ons tabela ACL no banco de dados.

Listagem 3.9 Adicionando usuários a um espaço

public JSONObject addMember(Solicitação de solicitação, Resposta de resposta) {


var json = new JSONObject(request.body());
var spaceId = Long.parseLong(request.params(":spaceId"));
var userToAdd = json.getString("username");
var perms = json.getString("permissões");

if (!perms.matches("r?w?d?")) { ❶
lançar novo IllegalArgumentException("permissões inválidas");
}
database.updateUnique( ❷
"INSERT INTO permissions(space_id, user_id, perms)" +
"VALUES(?, ?, ?);", spaceId, userToAdd, perms);

resposta.status(200);
retornar novo JSONObject()
.put("nome de usuário", userToAdd)
.put("permissões", permissões);
}

❶ Verifique se as permissões concedidas são válidas.

❷ Atualize as permissões do usuário na lista de controle de acesso.

Você pode adicionar uma nova rota ao seu método principal para permitir a adi-
ção de um novo membro por POSTing em /spaces/:spaceId/members . Abra
Main.java em seu editor novamente e adicione a seguinte nova rota e filtro de
controle de acesso ao método principal abaixo das rotas existentes:

before("/espaços/:espaçoId/membros",
userController.requirePermission("POST", "r"));
post("/spaces/:spaceId/members", spaceController::addMember);

Você pode testar isso adicionando o usuário demo2 ao espaço e permitindo que
ele leiamensagens:

$ curl -u demo:password
➥ -H 'Content-Type: application/json'
➥ -d '{"username":"demo2","permissions":"r"}'
➥ https://localhost:4567/ espaços/1/membros

{"permissões":"r","nome de usuário":"demo2"}
$ curl -u demo2:senha
➥ https://localhost:4567/spaces/1/messages/1

{"author":"demo","time":"2019-02-06T15:15:03.138Z","message":"Olá, mundo!","uri":"/space

3.6.5 Evitando ataques de escalonamento de privilégios

Istoacontece que o usuário demo2 que você acabou de adicionar pode fazer um
pouco mais do que apenas ler mensagens. As permissões no addMember méto-
dopermite que qualquer usuário com acesso de leitura adicione novos usuários
ao espaço e eles podem escolher as permissões para o novo usuário. Portanto, o
demo2 pode simplesmente criar uma nova conta para si e conceder a ela mais
permissões do que você originalmente concedeu, conforme mostrado no exemplo
a seguir.

Primeiro, eles criam o novo usuário:

$ curl -H 'Content-Type: application/json'


➥ -d '{"username":"evildemo2","password":"password"}'
➥ https://localhost:4567/users
➥ {"username" :"evildemo2"}

Em seguida, adicionam esse usuário ao espaço com permissões totais:

$ curl -u demo2:password
➥ -H 'Content-Type: application/json'
➥ -d '{"username":"evildemo2","permissions":"rwd"}'
➥ https://localhost:4567/ espaços/1/membros
{"permissions":"rwd","username":"evildemo2"}

Agora eles podem fazer o que quiserem, inclusive deletar suas mensagens:

$ curl -i -X ​
DELETE -u evildemo2:password
➥ https://localhost:4567/spaces/1/messages/1
HTTP/1.1 200 OK
...

O que aconteceu aqui é que, embora o usuário demo2 tenha recebido apenas per-
missão de leitura no espaço, ele pode usar essa permissão de leitura para adicio-
nar um novo usuário que tenha permissões totais no espaço. Isso é conhecido
como escalonamento de privilégios, em que um usuário com privilégios mais bai-
xos pode explorar um bug para obter privilégios mais altos.

DEFINIÇÃO Uma escalação de privilégio (ou elevação de privilégio) ocorre


quando um usuário com permissões limitadas pode explorar um bug no sistema
para conceder a si mesmo ou a outra pessoa mais permissões do que as
concedidas.

Você pode corrigir isso de duas maneiras gerais:

1. Você pode exigir que as permissões concedidas ao novo usuário não sejam mais
do que as permissões concedidas ao usuário existente. Ou seja, você deve garan-
tir que evildemo2 receba apenas o mesmo acesso que o usuário demo2.
2. Você pode exigir que apenas usuários com todas as permissões possam adicio-
nar outros usuários.
Para simplificar, você implementará a segunda opção e alterará o filtro de autori-
zação no addMember operação para exigir todas as permissões. Efetivamente, isso
significa que apenas o proprietário ou outros moderadores podem adicionar no-
vos membros a um espaço social.

Abra o arquivo Main.java e localize o filtro anterior que concede acesso para adi-
cionar usuários a um espaço social. Altere as permissões necessárias de r para
rwd da seguinte forma:

before("/espaços/:espaçoId/membros",
userController.requirePermission("POST", "rwd"));

Se você repetir o ataque com demo2 novamente, descobrirá que eles não são mais
capazes de criar nenhum usuário, muito menos um com privilégios elevados.

questionário

7. Qual código de status HTTP indica que o usuário não tem permissão para aces-
sar um recurso (em vez de não ser autenticado)?
1. 403 Proibido
2. 404 não encontrado
3. 401 não autorizado
4. 418 Eu sou um Bule
5. 405 Método não permitido

A resposta está no final do capítulo.


Respostas para perguntas do questionário

1. c. A limitação de taxa deve ser aplicada o mais cedo possível para minimizar os
recursos usados ​no processamento de solicitações.
2. b. o Retry-After cabeçalhoinforma ao cliente quanto tempo esperar antes de
tentar novamente as solicitações.
3. d, e e f. Um algoritmo de hash de senha segura deve usar muita CPU e memória
para dificultar a execução de força bruta por um invasore ataques de dicioná-
rio. Ele deve usar um sal aleatório para cada senha para evitar que um invasor
pré-compute tabelas de hashes de senha comuns.
4. e. As credenciais HTTP Basic são codificadas apenas em Base64, que, como você
se lembrará da seção 3.3.1, são fáceis de decodificar para revelar a senha.
5. c. O TLS não fornece proteções de disponibilidade por conta própria.
6. d. O princípio da separação de funções.
7. uma. 403 Proibido. Como você deve se lembrar do início da seção 3.6, apesar do
nome, 401 Unauthorized significa apenas que o usuário estánãoautenticado.

Resumo

Use a modelagem de ameaças com STRIDE para identificar ameaças à sua API.
Selecione os controles de segurança apropriados para cada tipo de ameaça.
Aplique limitação de taxa para mitigar ataques DoS. Os limites de taxa são me-
lhor aplicados em um balanceador de carga ou proxy reverso, mas também po-
dem ser aplicados por servidor para defesa em profundidade.
Habilite o HTTPS para todas as comunicações da API para garantir a confidenci-
alidade e integridade das solicitações e respostas. Adicione cabeçalhos HSTS
para informar aos clientes do navegador da Web que sempre usem HTTPS.
Use autenticação para identificar usuários e evitar ataques de falsificação. Use
um esquema seguro de hash de senha como Scrypt para armazenar senhas de
usuários.
Todas as operações significativas no sistema devem ser registradas em um log
de auditoria, incluindo detalhes de quem executou a ação, quando e se foi bem-
sucedida.
Aplicar o controle de acesso após a autenticação. ACLs são uma abordagem sim-
ples para impor permissões.
Evite ataques de escalonamento de privilégios considerando cuidadosamente
quais usuários podem conceder permissões a outrosusuários.

1.
A RateLimiter classe está marcada como instável no Guava, portanto pode mu-
dar em versões futuras.

2.
Alguns serviços retornam um status 503 Serviço indisponível. Ambos são aceitá-
veis, mas 429 é mais preciso, especialmente se você executar a limitação de taxa
por cliente.

3.
Infelizmente, as especificações HTTP confundem os termos autenticação e autori-
zação. Como você verá no capítulo 9, existem esquemas de autorização que não
envolvem autenticação.

4.
O nome de usuário não pode conter dois pontos.

5.
https://tools.ietf.org/html/rfc5802
6.
https://blog.cryptographyengineering.com/2018/10/19/lets-talk-about-pake/

Você também pode gostar