Se você já trabalhou em algum projeto Laravel com certeza já ter percebido como o framework entrega uma vasta gama de ferramentas uteis para o desenvolvimento. Mas, você já se perguntou se não pode estar deixando passar algum erro? Se ao utilizar essas ferramentas não acabou escapando algum detalhe simples, mas importante para garantir a segurança do seu sistema?
Neste artigo irei mostrar alguns erros "simples", mas muito comuns e fáceis de serem resolvidos.
Fazendo um deploy seguro 🚀
Ao tornar um projeto público na internet é necessário se atentar ao arquivo .env que irá para os ambientes de produção/homologação, ou seja lá qual for o cenário de implantação, pois as chaves APP_ENV
e APP_DEBUG
podem estar abrindo possíveis vulnerabilidades nestes ambientes.
Qual a utilidade dessas chaves?
- APP_ENV = Flag que irá indicar o ambiente atual da aplicação, ela pode receber valores como "local", "production", "staging", "testing", etc.
- APP_DEBUG = Flag que irá indicar se o ambiente atual está em modo de debug, ela pode receber um booleano.
Use essa configuração apenas para ambiente de desenvolvimento.
APP_ENV=local
APP_DEBUG=true
Use essa configuração para ambiente de produção.
APP_ENV=production
APP_DEBUG=false
Qual o problema de não usar a configuração certa em produção?
Deixar uma aplicação Laravel no modo debug permite a exibição de mensagens de erros detalhadas, o que é ótimo para desenvolvimento mas, péssimo para outros ambientes, pois pode exibir as seguintes informações sensíveis:
- Trechos de código.
- Versão do PHP e do Laravel. (Pode ser utilizado para explorar falhas de segurança conhecidas das versões)
- Informações do usuário e keys secretas.
- Senha do banco de dados.
- Estado da chave
APP_ENV
.
A chave indicando o ambiente estar com o valor "local" pode abrir um leque amplo de vulnerabilidades, que pode ir desde o Telescope, que fica aberto sem necessidade de login até o comportamento de alguma funcionalidade do sistema esteja utilizando o APP_ENV para determinar o fluxo. Como, por exemplo, a seguinte configuração para o consumo de uma api externa:
'api_service' => [
'key' => env('APP_ENV') === 'production' ? env('API_KEY_PROD') : env('API_KEY_DEV')
]
Validando inputs 🔍
Todo desenvolvedor já deve ter escutado em algum momento da vida "Não confie no usuário" e neste artigo você irá ver mais um motivo para pensar assim.
Vamos imaginar o seguinte cenário, você possui em seu e-commerce um formulário de edição de produtos disponível apenas para os funcionários que não são gerentes, porém esse formulário não possui o campo valor
disponível a fim de não permitir alterações por funcionários não autorizados.
Esta é a tabela produtos
id | valor | nome | quantidade | codigo | setor_id |
---|---|---|---|---|---|
1 | 100 | Panela | 4 | PAN10323 | 3 |
... | ... | ... | ... | ... | ... |
Estas são as regras de validação do seu Form Request
public function rules(): array
{
return [
'nome' => 'string|max:255',
'quantidade' => 'integer|min:0',
'codigo' => 'string|max:255|unique:itens,codigo',
'setor_id' => 'exists:setores,id',
];
}
E esta é a função utilizada no seu controller:
public function FuncionarioUpdateItem(UpdateItemRequest $request, $id)
{
$item = Item::findOrFail($id);
$result = $item->update($request->all());
return response()->json(['sucesso' => $result], 200);
}
Você verificou e os dados foram atualizados corretamente, as validações foram bem-sucedidas e tudo parece estar em ordem. No entanto, há uma grande falha nesse fluxo: estamos considerando que o formulário enviará apenas quatro campos na requisição.
Mas o que acontece se um usuário mal-intencionado alterar manualmente o HTML da página para adicionar o campo valor? Ou se um invasor até mesmo enviar a requisição por meio de uma plataforma de API, como o Postman, incluindo o campo valor a requisição?
Digamos que o usuário malicioso enviou os seguintes dados para a rota:
{
valor: 1,
nome: 'Panela barata',
}
Você iria ter a sua tabela produtos com o seguinte registro:
id | valor | nome | quantidade | codigo | setor_id |
---|---|---|---|---|---|
1 | 1 | Panela barata | 4 | PAN10323 | 3 |
... | ... | ... | ... | ... | ... |
Agora o produto que custava R$ 100 acabou disponível por R$ 1, e o problema está na forma de extrair os dados do request.
Errado ❌
$request->all();
- Assim é retornado TODOS os dados do request.
Certo ✅
$request->validated();
- Assim é retornado APENAS os dados validos do request.
Se o Controller do exemplo estivesse utilizando a forma correta de receber os dados, o mesmo exemplo de payload malicioso iria resultar no seguinte registro:
id | valor | nome | quantidade | codigo | setor_id |
---|---|---|---|---|---|
1 | 100 | Panela barata | 4 | PAN10323 | 3 |
... | ... | ... | ... | ... | ... |
Evitando assim qualquer dado não esperado!
Evitando SQL Injection 💉
Quando falamos de SQL Injection falamos de vulnerabilidades que dão possibilidade para invasores executar comandos SQL indesejados e imprevistos no banco de dados da aplicação.
Exemplo:
Considere que seu sistema possui uma funcionalidade de pesquisa de usuários que executa a seguinte query.
$query = "SELECT * FROM users WHERE username = '$username'";
Considere que a variável $user é dada pelo usuário realizando a busca, se o usuário inserir admin'; DROP TABLE users; --
irá gerar a seguinte consulta.
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
A consulta gerada é maliciosa e irá derrubar a tabela users
inteira gerando um dano gigantesco a integridade da aplicação.
Como proteger minha aplicação?
Os próprios Eloquent ORM e o Query Builder do Laravel possuem uma proteção robusta contra inputs maliciosos e caracteres potencialmente perigosos para suas consultas. Porem devemos tomar cuidado ao utilizar quaisquer métodos que aceitem queries raw, pois estes se mal utilizados, podem resultar em aberturas para injeção de inputs mal-intencionados.
Exemplos:
Errado ❌
$email = $request->email;
DB::select("SELECT * FROM users WHERE email = '" . $email . "'");
Neste caso estamos passando diretamente o input do usuário para a consulta via concatenação de strings, que não irá realizar nenhuma sanitização de parâmetros.
Certo ✅
$email = $request->email;
User::where('email', $email)->get();
Desta forma utilizamos a proteção nativa do Laravel, passando o input como parâmetro da função que irá construir a query.
Certo ✅
$email = $request->email;
DB::select("SELECT * FROM users WHERE email = ?", [$mail]);
Supondo que você tenha realmente a necessidade de utilizar um método que aceite consulta raw, passe os parâmetros através de bindings. Desta forma, o Laravel irá impedir que o input seja interpretado como código SQL e manter a query segura.
Não se esqueça ⚠
Sempre use bindings quando o assunto for consultas com SQL bruto no Laravel.
Utilizando Rate Limit 🚧
Limitar quantas requisições podem ser realizadas em um determinado tempo é essencial para proteger a sua aplicação contra alguns tipos de ataques, como, por exemplo:
- Brute Force: Tentativa de "adivinhar" senhas e credenciais realizando inúmeras requisições com combinações possíveis até obter sucesso.
- DDoS: Tentativa de sobrecarregar o servidor com uma quantidade massiva de requisições em um curto período.
É recomendável considerar adicionar esta proteção em rotas relacionadas a formas de autenticação ou rotas com uso de token aleatório privado.
Como aplicar?
No arquivo AppServiceProvider.php
você irá criar dentro da função boot
o seu RateLimiter:
RateLimiter::for('default-limiter', function (Request $request) {
return Limit::perMinute(10);
});
Pronto, agora com nosso limiter definido iremos aplicar a nossa rota sensível.
Route::middleware('throttle:default-limiter')->group(function () {
Route::get('/private/{token}', [TokenController::class, 'privateThing']);
});
Considere que esta rota recebe um token gerado aleatoriamente para exibir um dado privado do seu portador. Desta forma estamos definindo um limite de 10 requisições por minuto, evitando um possível ataque por força bruta para adivinhar o token secreto e assim obter acesso à informação privada.
Ainda é possível definir regras específicas e personalizadas para a limitação como, por exemplo, por IP:
RateLimiter::for('default-limiter', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
Essas limitações são usualmente baseadas em IP e no usuário logado, e são muito importantes para evitar diversos tipos de ataques, inclusive ataques DDoS feito com diversos IPs. Lembre-se sempre de criar regras de limitação robustas!
Se você já utiliza algum kit de autenticação pronto do Laravel como o Breeze ou o Jetstream então parabéns! Suas rotas de login já estão protegidas com Rate Limit.
Atenção ⚠
Preste bastante atenção ao definir as limitações para não afetar o uso normal dos usuários do seu sistema. Em casos de rotas com um fluxo maior de requests é recomendável utilizar o Per-Second Rate Limiting feature do Laravel 11.
Conclusão
Devemos manter sempre um olhar atento para questões de segurança durante o desenvolvimento, pois pequenas decisões ou deslizes aparentemente insignificantes podem resultar em grandes problemas no futuro.
Muito obrigado por ler até aqui! ❤
Top comments (6)
Ótimo post, primo! Só com essas regras, tu consegue manter a aplicação bem segura.
Btw, adicionei a tag #braziliandevs pra outros BR’s acharem seu artigo.
Muito obrigado pelo apoio primo!
Você é brabo, inspiração demais ❤
Ótimo post primo! Ce é foda
Valeu demais primo!
Tamo junto ❤
Além dessas práticas eu adiciono sleep de 1 segundo em todas as rotas relacionadas a login. Tornando pouco incomodo para humanos mas ineficiente para máquinas de força bruta ou roubo de dados.
Muito massa! , essa dica de utilizar bindings quando for utilizar query bruta foi top, vou começar a utilizar!