Security Tip: Prohibiting Destructive Commands on Production
[Tip #83] It's important to be paranoid when it comes to production environments - because if you forget you're logged into prod, you may end up dropping a database... or worse! 😱
Way back in Security Tip #6 I wrote about manually disabling debug and testing Artisan commands from running in production (or staging) using something like this:
public function handle()
{
if (! app()->isLocal()) {
$this->error('This command can only be used in dev!');
return Command::INVALID;
}
// ...
}
As a refresher, you should disable any Artisan commands that shouldn't be run in production (or staging!) from working in those environments. This ensures they cannot be accidently triggered (i.e. if a dev forgets they've SSH'ed into a prod server!) and have some destructive effect. Such as dropping a database, resetting user passwords, etc.
As of Laravel 11.9 (released in May 2024), there is a new option we can use to prevent destructive Artisan commands from being run - without needing a custom if
inside each handle
method! It's done through a new Illuminate\Console\Prohibitable
trait, which allows you to toggle when the command can be run on each command.
To start using it, first add the Prohibitable
trait onto your Artisan command:
use Illuminate\Console\Command;
use Illuminate\Console\Prohibitable;
class ResetUserPasswordsCommand extends Command
{
use Prohibitable;
// ...
}
And then toggle the command in a Service Provider:
public function boot(): void
{
// Prevent from running in production.
ResetUserPasswordsCommand::prohibit($this->app->isProduction());
}
This new trait has already been added to Laravel's db:wipe
, migrate:fresh
, migrate:refresh
, and migrate:reset
commands. Allowing you to prevent them from accidently being run in production - which I would suggest is a really good idea!
public function boot(): void
{
FreshCommand::prohibit($this->app->isProduction());
RefreshCommand::prohibit($this->app->isProduction());
ResetCommand::prohibit($this->app->isProduction());
WipeCommand::prohibit($this->app->isProduction());
// OR prohibt them all in one go
DB::prohibitDestructiveCommands($this->app->isProduction());
}
Btw, the ::prohibit()
method defaults to true
when nothing is specified, allowing you to completely lock out a command if you need to.
You can check out the trait on GitHub and find more details in the Laravel News article. Also, a huge thanks to Jason McCreary and Joel Clermont for adding this awesome feature to the framework. 🙂