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


If you found this security tip useful? 👍
Subscribe now to get weekly Security Tips straight to your inbox, filled with practical, actionable advice to help you build safer apps.

Want to learn more? 🤓
Upgrade to a Premium Subscription for exclusive monthly In Depth articles, or support my work with a one-off tip! Your support directly funds my security work in the Laravel community. 🥰

Need a second set of eyes on your code?
Book in a
Laravel Security Audit and Penetration Test today! I also offer budget-friendly Security Reviews too.

Finally, connect with me on Bluesky, or other socials, and check out Practical Laravel Security, my interactive course designed to boost your Laravel security skills.