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! 😱

Security Tip: Prohibiting Destructive Commands on Production

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. 🙂


🕵️
Want me to hack into your app and tell you how I did it, so you can fix it before someone else finds it? Book in a Laravel Security Audit and Penetration Test!
🥷
Looking to dive deeper into Laravel security? Check out Practical Laravel Security, my hands-on security course that uses interactive hacking challenges to teach you about how vulnerabilities work, so you can avoid them in your own code!