“...I've been working since 2008 with Ruby / Ruby on Rails, love a bit of Elixir / Phoenix and learning Rust. I also poke through other people's code and make PRs for OpenSource Ruby projects that sometimes make it. Currently working for InPay...”

Rob Lacey (contact@robl.me)
Senior Software Engineer, Brighton, UK

Rails 5.2 RemoteIP middleware

We have a broken spec for testing IP Spoofing, turns out it is raising an error.

describe 'IP Spoofing' do
  it 'does not raise an IP Spoofing error' do
    expect do
      get '/', headers: {
        'HTTP_CLIENT_IP' => '10.95.157.143',
        'HTTP_X_FORWARDED_FOR' => '62.172.169.17, 141.101.99.166, 192.168.255.26'
      }
      expect(response.status).to eq(400)
    end.not_to raise_error
  end
end

… we have middleware that captures the Exception and returns a 400 Bad Request status code instead of exploding whilst upgrading to Rails 5.2. Nice 400 errors are better than Exception(s)

module Rack
  class Spoofing < CustomMiddleware
    def call(env)
      @app.call(env)
    rescue ActionDispatch::RemoteIp::IpSpoofAttackError
      [400, {}, []]
    end
  end

  Rails.application.config.middleware.insert_before Rack::Head, Spoofing
end

We insert our Spoofing middleware just ahead of Rack::Head. We can see this in rake middleware

Robs-MacBook-Pro:some rl$ rake middleware
use Webpacker::DevServerProxy
use Rack::Ping
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use RequestStore::Middleware
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Spoofing
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
use Remotipart::Middleware
use Rack::Jayson
use PDFKit::Middleware
use Warden::JWTAuth::Middleware
run Some::Application.routes

Just wondering where that Spoofing exception is raised

Robs-MacBook-Pro:some rl$ bundle show --paths | xargs grep -r IpSpoofAttackError
/Users/rl/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-5.2.6/lib/action_dispatch/middleware/remote_ip.rb:    class IpSpoofAttackError < StandardError; end
/Users/rl/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-5.2.6/lib/action_dispatch/middleware/remote_ip.rb:          raise IpSpoofAttackError, "IP spoofing attack?! " \

There it is, it’s raised by RemoteIP, which we can see from our middleware stack is much further down the middleware stack than Rack::Head and so in Rails 5 at least runs earlier in the chain. No wonder we can’t rescue it now. In fact it makes sense to bomb out early rather than waste effort crunching cookies, etc if we don’t need to.

If we look at the Rails 5.1 middleware stack I was almost expecting our Spoofing middleware to appear before RemoteIp. It clearly doesn’t.

use Webpacker::DevServerProxy
use Rack::Ping
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use BadMultipartFormDataSanitizer
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00007f8190611610>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use RequestStore::Middleware
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use Rack::Spoofing
use ActionDispatch::ParamsParser
use Remotipart::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Warden::Manager
use PDFKit::Middleware
use Warden::JWTAuth::Middleware
use Rack::Jayson
run Some::Application.routes

I’m actually wondering if the 400 response even comes from our Spoofing middleware in Rails 5.

Ah, no it doesn’t. Another application error is creating it. Looks like its reaching the application and not raising the spoofing error anyway :S

The RemoteIp middleware initializer looks like this in Rails 4. Turns out check_ip_spoofing is false, something turned it off.

def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
      binding.pry
      @app = app
      @check_ip = check_ip_spoofing
      @proxies = if custom_proxies.blank?
        TRUSTED_PROXIES
      elsif custom_proxies.respond_to?(:any?)
        custom_proxies
      else
        Array(custom_proxies) + TRUSTED_PROXIES
      end
    end

Looks like there’s an environment setting to turn it on in the first place. Rails 5.x probably has a different default..

module Rails
  class Application
    class DefaultMiddlewareStack
      attr_reader :config, :paths, :app

      def initialize(app, config, paths)
        @app = app
        @config = config
        @paths = paths
      end

      def build_stack
          # ...
          middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
          # ... 
      end
  end
end

Well, I’m not turning it on in Rails 4 now. We want rid of it anyway. I am however going to move the middleware to sit just in front of RemoteIp so that it can actually catch the damn thing and make sure our 400 is actually coming from the thing we’re testing.

Rails.application.config.middleware.insert_before ActionDispatch::RemoteIp, Spoofing