O princípio do menor privilégio aplicado a firewalls do Symfony
O conceito que descreve o isolamento de partes críticas de um sistema para aumentar a segurança é conhecido como princípio do menor privilégio ou princípio do privilégio mínimo. Este princípio sugere que cada componente ou parte do sistema deve possuir o mínimo de privilégios possível para desempenhar suas funções específicas.
Ao aplicar o princípio do menor privilégio, é possível reduzir a superfície de ataque do sistema, limitando o acesso e a exposição de partes sensíveis ou críticas. Isso significa que, mesmo que uma parte do sistema seja comprometida, o acesso a outras partes é restrito, minimizando os danos potenciais.
O princípio do menor privilégio não é uma bala de prata e não resolve todos os problemas, o conceito possui prós e contras e isso deve ser levado em consideração antes de aplicá-lo.
Cache HTTP
O princípio faz uso do cache HTTP para melhorar o desempenho da sua API, com uma política de cache eficiente você pode fazer com que respostas comuns sejam guardadas no seu proxy reverso e que a sua aplicação não seja atinginda a todo momento. Para que a requisição seja cacheável ela deve atender aos seguintes requisitos:
- Não conter dados privados 1
- Não estar associado a nenhum usuário
- Ser pública
- Reposta mudar com pouca frequência
- Não oferecer riscos para a aplicação
Se você possui rotas na sua API cujo resultado muda com pouca frequência e cujo os dados não revelem segredos da aplicação, então você pode aplicar o princípio do menor privilégio nessas rotas. Alguns exemplos de dados que posso mencionar são:
- Cidades do Brasil
- Artigos de um blogue
- Perfis públicos de usuários
- Músicas de um artista
Todo e qualquer dado (mesmo divulgado) que não oferece quebras de sigilo, pode ser cacheado em um proxy reverso como um nginx, ingress ou cloudflare, e este mesmo dado poderá ser mais tarde armazenado em cache no navegador do usuário sem qualquer risco de vazamento.
Onde está o privilégio mínimo?
Aqui vem a parte interessante: A aplicação não terá acesso aos dados do usuário, o privilégio mínimo está na aplicação em si.
Para que uma resposta seja pública o framework do Symfony por questões de segurança não permite que a sessão do usuário seja inicializada, em outras palavras, se o usuário enviar algum cookie durante a requisição o framework não fará a identificação deste usuário como sempre costuma fazer. A nível de aplicação, toda tentativa de buscar o usuário autenticado falhará de alguma maneira e o princípio do menor privilégio será quebrado. De duas uma, ou a resposta da aplicação se tornará privada, ou o framework irá disparar um erro 500 avisando que a sessão não pôde ser iniciada. Em todos os casos não existe um risco iminente de vazamento de dados pelo mau uso do princípio, pois o próprio framework toma conta dos mecanismos básicos de segurança.
Além da falta de acesso à sessão do usuário é contado também com o bom senso do programador, onde ele ou ela, busca não vincular as respostas públicas da aplicação com nenhum dado sigiloso dos usuários.
Firewalls do Symfony
O princípio é aplicado através de firewalls do Symfony onde um firewall específico é designado à todas as rotas públicas, este firewall público será o responsável por abranger todas as rotas que seguem o princípio do menor privilégio.
Por padrão o Symfony vem com a seguinte configuração de firewall:
---
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
json_login:
check_path: login
access_control:
- { path: ^/api/admin/, roles: ROLE_USER }
- { path: ^/, roles: PUBLIC_ACCESS }
Onde dev é o firewall utilizado no ambiente de desenvolvimento e main é o firewall que contempla toda a aplicação e política de acesso. O firewall principal é quem dita o tipo de usuário que pode acessar cada parte da sua aplicação.
Dentro de toda requisição no contexto daquele firewall o Symfony tentará identificar o usuário através dos cookies e isso nem sempre é o caminho mais desejável. Ao identificar o usuário pelos cookies o sistema estará buscando ele no banco de dados no decorrer de todas as requisições. Isso consumirá recursos da base de dados pois é feito um SELECT toda vez que o usuário chama a API, isso deixa a aplicação muito mais lenta dependendo do esquema e o volume na sua base de dados. Além da sobrecarga no banco, a requisição se torna privativa e você não consegue reaproveitar respostas para requisições semelhantes.
Se os dados que você precisa trabalhar são públicos e não dependem do usuário autenticado, então você pode aplicar o princípio do menor privilégio através de um novo firewall no Symfony.
O Firewall público
Você pode criar um novo firewall no Symfony e fazê-lo operar no modo stateless. Neste modo todas as requisições gerenciadas pelo firewall não utilizarão os cookies e o Symfony não fará com que os usuários recebam novos cookies. Além da identificação do usuário, nós estaremos poupando os recursos da base de dados. Não é necessário identificar o usuário em todas as requisições, portanto, não é preciso a autenticação em todas partes. Algumas partes da sua API podem ser abertas de propósito afim de ganhar desempenho, o Symfony não precisa se preocupar com o controle de acesso, ele só cospe o resultado e pronto, sua resposta estará pública e cacheável.
Eis um exemplo de como ficaria a configuração no arquivo security.yaml da sua
aplicação.
---
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
json_login:
check_path: login
public:
anonymous: true
security: false
request_matcher: App\Security\RequestMatchers\PublicFirewallMatcher
stateless: true
access_control:
- { path: ^/api/admin/, roles: ROLE_USER }
- { path: ^/api/autocomplete/name-suggestions/, roles: PUBLIC_ACCESS }
- { path: ^/api/blog-post/find/by-id/\d+, roles: PUBLIC_ACCESS }
- { path: ^/, roles: PUBLIC_ACCESS }
Quais partes da minha API eu posso abrir? Isso vai do bom senso do programador. Se os dados são de fato públicos então você pode torná-los públicos, a exemplo da configuração acima.
Você pode querer me perguntar: Mas isso não torna a API suscetível a ataques IDORS? 2 Bom, se o dado é público em primeiro lugar então não há porquê se preocupar com ataques IDORS, porém se isso for um problema para você então você pode usar um valor impossível de adivinhar, como um identificador universal (UUID) ou número aleatório.
Se você notou na configuração acima nós fizemos uma referência a uma classe PHP. Esta classe é a responsável por dizer se a requisição deve ou não ser contemplada pelo firewall público. Nela você tem a liberdade para implementar qualquer regra de negócio, mas para fins de simplicidade nós iremos apenas checar o local da requisição de modo que a lógica fique semelhante ao arquivo de configuração yaml.
<?php
declare(strict_types=1);
namespace App\Security\RequestMatchers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use function in_array;
final readonly class PublicFirewallMatcher implements RequestMatcherInterface
{
/**
* @var string[]
*/
private array $staticRoutes = [
'/api/autocomplete/name-suggestions/',
];
public function matchRequest(Request $request): bool
{
return in_array(
$request->getPathInfo(),
$this->staticRoutes,
true
);
}
}
A regra é a seguinte, essa classe deverá retornar verdadeiro para toda rota da aplicação que deve ser pública e stateless3. Você pode usar textos simples para fazer a comparação ou expressões regulares para rotas dinâmicas.
<?php
declare(strict_types=1);
namespace App\Security\RequestMatchers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use function in_array;
final readonly class PublicFirewallMatcher implements RequestMatcherInterface
{
/**
* @var string[]
*/
private array $staticRoutes = [
'/api/autocomplete/name-suggestions/',
];
/**
* @var string[]
*/
private array $routePatterns = [
'/^\/api\/blog-post\/find\/by\-id/\d+/',
];
public function matchRequest(Request $request): bool
{
$matchesStaticRoutes = in_array(
$request->getPathInfo(),
$this->staticRoutes,
true
);
$matchesDynamicRoutes = (bool) count(
array_filter(
$this->routePatterns,
fn (string $routePattern): bool => 1 ===
preg_match($routePattern, $request->getPathInfo())
)
);
return $matchesStaticRoutes || $matchesDynamicRoutes;
}
}
O lado bom desta abordagem é que você pode criar testes unitários para o seu firewall, garantindo maior segurança até durante a auditoria!
Agora com o firewall devidamente configurado nós podemos criar as rotas propriamente ditas.
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class NameSuggestionController extends AbstractController
{
#[
Route(
'/api/autocomplete/name-suggestions/',
name: 'api_autocomplete_name_suggestions',
stateless: true, // <-- Notice the stateless option
methods: [Request::METHOD_GET]
)
]
public function findNameSuggestions(): JsonResponse
{
return $this
->json('ok')
->setPublic()
->setMaxAge(600);
}
}
Um detalhe diferente das rotas comuns é a opção stateless definida como verdadeiro, isso é muito importante pois vai garantir que o framework não inicialize a sessão do usuário sem intenção, caso a inicialização seja forçada — através do método $this->getUser() —, então o próprio Symfony mostrará um erro 500. Isso garantirá que a resposta seja pública e cacheável.
Benchmarks
É possível ver a diferença no número de consultas no banco de dados de uma aplicação proprietária com o código semelhante ao dos exemplos dessa postagem.

Figura 1 - Consultas de banco em uma rota com autenticação
E agora o mesmo endpoint sem autenticação.

Figura 2 - Consultas de banco sem autenticação
Todas as outras consultas buscavam informações sobre o usuário atualmente logado e suas permissões de acesso que ficam segregadas em várias tabelas.

Figura 3 - Cache HTTP em ação
Também é possível ver a forma como o navegador se comporta ao receber uma resposta como esta, da segunda vez em diante ele sempre vai buscar do cache ao invés de repetir a requisição. Uma combinação de consultas de banco que foram economizadas + cache de navegador resultam em uma busca muito mais rápida.
É isso, espero que este artigo tenha dirimido suas dúvidas sobre o princípio do privilégio mínimo e até a próxima edição.
Entende-se como dado privado todos os dados pessoais presentes na URL da requisição ou na resposta, mesmo que o dado pertença a um grupo privado ele também é sigiloso. ↩︎
Insecure Direct Object Reference é uma vulnerabilidade em aplicações web onde uma referência aos dados é fácil de adivinhar, por exemplo uma rota de API que possui identificadores com números sequenciais. ↩︎
Stateless no contexto do roteador do Symfony significa sem acesso a cookies. ↩︎