There are many different flavors of web cache poisoning discovered by Security Researcher James Kettle. Read on for an explanation of one I’ve run across…

What is a Fat GET/HEAD Request? A GET or HEAD request is “fat” when it has a request body. It’s unexpected! Typically one sees a request body with a POST or PUT request because the body contains form data. The HTTP specification says that including a request body with GET or HEAD requests is undefined. You can do it, and it’s up to the application to figure out what that means. Sometimes it’s bad!

You can get a sense of the applications that intentionally support Fat Requests (and how grumpy it makes some people) by reading through this Postman issue.

Fat Requests can lead to CDN and cache poisoning in Rails. CDNs and caching web proxies (like Varnish) are frequently configured to cache the response from a GET or HEAD request based solely on the request’s URL and not the contents of the request body (they don’t cache POSTs or PUTs at all). If an application isn’t deliberately handling the request body, it may cause unexpected content to be cached and served.

For example, you have a /search endpoint:

  • GET /search shows a landing page with some explanatory content
  • GET /search?q=foo shows the search results for “foo”.
  • Here’s what a Fat Request looks like:

    GET /search     <== the url for the landing page
    
    q=verybadstuff  <== oh, but with a request body
    

In a Rails Controller, parameters (alias params) merges query parameters (that’s the URL values) with request parameters (that’s the body values) into a single data structure. If your controller uses the presence of params[:q] to determine whether to show the landing page or the search results, it’s possible that when someone sends that Fat Request, your CDN may cache and subsequently serve the results for verybadstuff every time someone visits the /search landing page. That’s bad!

Here’s how to Curl it:

curl -XGET -H "Content-Type: application/x-www-form-urlencoded" -d "q=verybadstuff" http://localhost:3000/search

Here are 3 ways to fix it…

Solution #1: Fix at the CDN

The most straightforward place to fix this should be at the caching layer, but it’s not always easy.

With Cloudflare, you could rewrite the GET request’s Content-Type header if it is application/x-www-form-urlencoded or multipart/form-data. Or use a Cloudflare Worker to drop the request body.

Varnish makes it easy to drop the request body for any GET request.

Other CDNs or proxies may be easier or more difficult. It depends!

Update via Mr0grog: AWS Cloudfront returns a 403 by default.

Solution #2: Deliberately use query_parameters

Rails provides three different methods for accessing parameters:

  • query_parameters for the values in the request URL
  • request_parameters ) for the values in the request body
  • parameters (alias params) for the problematic combination of them both. Values in query_parameters take precedence over values in request_parameters when they are merged together.

Developers could be diligent and make sure to only use query_parameters in #index or #show , or get routed actions. Here’s an example from the git-scm project.

Solution #3: Patch Rails

Changes were proposed in Rails to not have parameters merge in the body values for GET and HEAD requests; it was rejected because it’s more a problem with the upstream cache than it is with Rails.

You can patch your own version of Rails. Here’s an example that patches the method in ActionDispatch::Request:

# config/initializers/sanitize_fat_requests.rb
module SanitizeFatRequests
  def parameters
    params = get_header("action_dispatch.request.parameters")
    return params if params

    if get? || head?
      params = query_parameters.dup
      params.merge!(path_parameters)
      set_header("action_dispatch.request.parameters", params)
      params
    else
      super
    end
  end
  alias :params :parameters
end

ActionDispatch::Request.include(SanitizeFatRequests)

# Some RSpec tests to verify this
require 'rails_helper'

RSpec.describe SanitizeFatRequests, type: :request do
  it 'does not merge body params in GET requests' do
    get "/search", headers: {'CONTENT_TYPE' => 'application/x-www-form-urlencoded'}, env: {'rack.input': StringIO.new('q=verybadstuff') }

    # verify that the request is correctly shaped because
    # the test helpers don't expect this kind of request
    expect(request.request_parameters).to eq("q" => "verybadstuff")
    expect(request.parameters).to eq({"action"=>"panlexicon", "controller"=>"search"})

    # the behavioral expectation
    expect(response.body).not_to include "verybadstuff"
  end
end