Rack::Protection::MaximumCookie
Some bugs in Rack may cause cookies to be silently dropped—or worse, truncated, leading to token leakage (i.e. transmission of a private session over a insecure connection), cross-site scripting vulnerabilities, and/or cross-site request forgery vulnerabilities. This gem provides a middleware that tries to prevent these scenarios from occurring.
Caveats
Most modern browsers no longer have a per-domain cookie size limit, so if you only care about modern browsers, go ahead and set
:per_domain?
to false. You'll benefit from the per-domain cookie limit and per-cookie bytesize limit (since most browsers still have some form of these limits, and Rack's built-in check for them is nonexistent in the former case and not implemented correctly in the latter).Browsers that do have per-domain cookie limits enforce these limits client-side, cumulatively over time. If you were to set a new cookie, every few minutes, one at a time, eventually you'd hit a limit. Or another service could be setting cookies for the domain your service listens on. If this is a concern for your app, see the
:stateful?
option below.In practice, your service is most likely setting the same cookies, for the same domains, every response, all in the same response, and it knows about all the cookies being set, so the default behavior of this gem should be appropriate in those cases.
Installation
Add this line to your app's Gemfile:
gem 'rack-protection-maximum_cookie'
And then execute:
$ bundle
Or install it yourself as:
$ gem install rack-protection-maximum_cookie
Usage
Add this statement to your Rackup script or middleware-capable Rack app such as Sinatra:
use Rack::Protection::MaximumCookie
Or Rails:
TODO: Somebody please tell me how to insert this and before or after what in Rails' middleware stack.
This middleware raises exceptions, so you'll want to use an error-reporting service like Sentry, Rollbar, or Airbrake, and insert this middleware after it.
Advanced
Rack::Protection::MaximumCookie accepts the following use
-time options:
:limit
Integer
Maximum number of cookies per domain. 50 cookies by default. Set to a negative number to disable.
:bytesize_limit
Integer
Maximum size—in bytes—of cookies per domain (if :per_domain?
is
set to true), or the maximum size of each cookie (if :per_domain?
is set to
false). 4,096 bytes by default. Set to a negative number to disable.
:overhead
Integer
Overhead—in bytes—per cookie. Three (3) bytes by default. Set to zero to disable.
:per_domain?
Boolean
If true, apply the bytesize limit (e.g. 4,096 bytes—minus any per-cookie overhead) per domain. This is the default behavior.
If false, apply the bytesize limit (e.g. 4,096 bytes—minus any overhead) per cookie.
:strict?
Boolean
If false, each sub-domain gets its own quota, separate from its second-level domain. This is the default behavior.
If true, :per_domain?
is forced to true, and each second-level domain's
cookies count towards its sub-domains' quotas. For example, if you have
cookies for example.org totaling 4,000 bytes, you wouldn't be able to
set an additional 100-byte cookie on foo.example.org in the same
request.
:stateful?
Boolean
If false, the cookies are evaluated for each response in isolation, i.e. statelessly. This is the default behavior.
If true, :strict?
is forced to true, and Rack::Protection::MaximumCookie
will attempt to compensate for cookies that were set for the domain in
previous responses (by this or other web services) but are not present in the
current response.
This mechanism has its limits. First of all, browsers don't send cookie directives in request headers—only key=value pairs—so in order to compute the actual size of these cookies, Rack::Protection::MaximumCookie must estimate the upper bound of the bytesize of the directives in the original Set-Cookie headers. This should work reasonably well as long as your app doesn't rely on any non-standard directives or other quirky browser behavior.
Secondly, browsers won't send cookies if their paths don't match the request path. This means that if you're setting two cookies, one for example.org/foo and one for example.org/bar, requests to either path won't include the other's cookie. That defeats this mechanism, as both cookies count toward example.org's quota, but they aren't able to be properly accounted for in the request. The only "workaround" is to architect your app such that all cookies are set under a path common to all routes. Unfortunately, this may lead to other security issues.
This is an ugly wart on HTTP and the reason why modern browsers don't have per-domain cookie size limits and drop cookies instead of truncating them.
Block Argument
If you don't want to raise exceptions, only want to raise exceptions under certain conditions, or want to customize the exception, you can modify the behavior by passing a block to the middleware initializer:
use Rack::Protection::MaximumCookie do |env|
raise MyCustomError, 'Someone broke the cookie jar!' if env['foo.bar']
end
If the block returns a truthy value, the default exception will be raised:
use(Rack::Protection::MaximumCookie) { |env| env['foo.bar'] }
Keep in mind that the block receives the mutated response env
.
I'm interested in hearing use-cases for this feature and I'm open to passing additional arguments to the block. Open a new issue to document and discuss.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run
rake spec
to run the tests. You can also run bin/console
for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To
release a new version, update the version number in version.rb
, and then run
bundle exec rake release
, which will create a git tag for the version, push
git commits and tags, and push the .gem
file to
rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mwpastore/rack-protection-maximum_cookie.
Please let me know if I've made any invalid assumptions or conclusions about the HTTP specification or older browser behavior.
License
The gem is available as open source under the terms of the MIT License.