Disabling dangerous redis commands in Production redux

in #programming5 years ago (edited)

redis
I recently read a great article from Honeybadger that muses on what could happen to a production environment if a fat fingered developer did something like:
Redis.current.flushdb.

This scared me enough that I decided we needed something like this at Weedmaps. The only problem is that we want to keep these commands available in lower environments. We also do not allow new code into our codebases without tests. We make exceptions but they are rare, since we use our tests as documentation and a sanity check.

Here is my adaptation to this [gist]~~~ embed:c1ff2657c150df6fb1257398b1d2716b) which was provided in the Honeybadger article above: gist metadata:am9zaHVhcC9jMWZmMjY1N2MxNTBkZjZmYjEyNTczOThiMWQyNzE2Yikgd2hpY2ggd2FzIHByb3ZpZGVkIGluIHRoZSBIb25leWJhZGdlciBhcnRpY2xlIGFib3ZlOg== ~~~

############### config/initializers/redis.rb #########################
require 'redis'
require 'redis_dangerous_commands'

class Redis
  prepend DangerousCommands
end

############### lib/redis_dangerous_commands.rb #########################
module DangerousCommands
  class ProhibitedCommandError < StandardError; end

  def self.prepended(base)
    return unless Rails.env.production?

    base.send(:define_method, :flushdb) do
      raise ProhibitedCommandError, error_message('EMPTY THE ENTIRE DATABASE')
    end

    base.send(:define_method, :flushall) do
      raise ProhibitedCommandError, error_message('FLUSH ALL DATABASES')
    end
  end

  def error_message(inner_message)
    "DANGEROUS destructive operation. If you really want to #{inner_message} do it from `redis-cli`."
  end
end

############### spec/lib/redis_dangerous_commands_spec.rb #########################
describe 'DangerousCommands' do
  context 'Production' do
    let(:dumb) { Dummy.new }

    before do
      allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
      class Dummy
        prepend DangerousCommands
      end
    end

    it 'raises ProhibitedCommandError' do
      expect{ dumb.flushdb }.to raise_error DangerousCommands::ProhibitedCommandError
    end

    it 'raises ProhibitedCommandError' do
      expect{ dumb.flushall }.to raise_error DangerousCommands::ProhibitedCommandError
    end
  end

  context 'Non-production' do
    let(:dumb) { DifferentDummy.new }

    before do
      class DifferentDummy
        prepend DangerousCommands
      end
    end

    it 'does not respond to flushdb' do
      expect(dumb).not_to respond_to(:flushdb)
    end

    it 'does not respond to flushall' do
      expect(dumb).not_to respond_to(:flushall)
    end
  end
end

And a link to a gist if that's easier for ya (ᵔᴥᵔ)

The basic idea is that we needed a way to a) optionally include the disabled override methods based on the Rails.env
and b) test this. The trick was figuring out how to stub Rails.env in an initializer spec. Turns out it isn't possible since the environment loads those initializers as it boots up to run your specs. By time you stub out your Rails.env it is too late.

This approach only defines those methods in the prepended hook provided by Ruby. Since, this module lives in lib
we can stub our Rails.env and prepend our module into some dummy classes. If the dummy class receives those methods in when we stub out a production env then we know we are in business.

~(˘▾˘~)

Sort:  

Congratulations @davidpm! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

Use your witness votes and get the Community Badge
Vote for @Steemitboard as a witness to get one more award and increased upvotes!