This repository has been archived by the owner on May 16, 2021. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from jkowens/cookie_tossing
Add cookie tossing protection
- Loading branch information
Showing
4 changed files
with
154 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
require 'rack/protection' | ||
require 'pathname' | ||
|
||
module Rack | ||
module Protection | ||
## | ||
# Prevented attack:: Cookie Tossing | ||
# Supported browsers:: all | ||
# More infos:: https://github.com/blog/1466-yummy-cookies-across-domains | ||
# | ||
# Does not accept HTTP requests if the HTTP_COOKIE header contains more than one | ||
# session cookie. This does not protect against a cookie overflow attack. | ||
# | ||
# Options: | ||
# | ||
# session_key:: The name of the session cookie (default: 'rack.session') | ||
class CookieTossing < Base | ||
default_reaction :deny | ||
|
||
def call(env) | ||
status, headers, body = super | ||
response = Rack::Response.new(body, status, headers) | ||
request = Rack::Request.new(env) | ||
remove_bad_cookies(request, response) | ||
response.finish | ||
end | ||
|
||
def accepts?(env) | ||
cookie_header = env['HTTP_COOKIE'] | ||
cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s } | ||
cookies.each do |k, v| | ||
if k == session_key && Array(v).size > 1 | ||
bad_cookies << k | ||
elsif k != session_key && Rack::Utils.unescape(k) == session_key | ||
bad_cookies << k | ||
end | ||
end | ||
bad_cookies.empty? | ||
end | ||
|
||
def remove_bad_cookies(request, response) | ||
bad_cookies.each do |name| | ||
cookie_paths(request.path).each { |path| response.set_cookie name, empty_cookie(request.host, path) } | ||
end | ||
end | ||
|
||
def redirect(env) | ||
request = Request.new(env) | ||
warn env, "attack prevented by #{self.class}" | ||
[302, {'Content-Type' => 'text/html', 'Location' => request.path}, []] | ||
end | ||
|
||
def bad_cookies | ||
@bad_cookies ||= [] | ||
end | ||
|
||
def cookie_paths(path) | ||
path = '/' if path.to_s.empty? | ||
paths = [] | ||
Pathname.new(path).descend { |p| paths << p.to_s } | ||
paths | ||
end | ||
|
||
def empty_cookie(host, path) | ||
{:value => '', :domain => host, :path => path, :expires => Time.at(0)} | ||
end | ||
|
||
def session_key | ||
@session_key ||= options[:session_key] | ||
end | ||
end | ||
end | ||
end |
74 changes: 74 additions & 0 deletions
74
rack-protection/spec/lib/rack/protection/cookie_tossing_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
describe Rack::Protection::CookieTossing do | ||
it_behaves_like "any rack application" | ||
|
||
context 'with default reaction' do | ||
before(:each) do | ||
mock_app do | ||
use Rack::Protection::CookieTossing | ||
run DummyApp | ||
end | ||
end | ||
|
||
it 'accepts requests with a single session cookie' do | ||
get '/', {}, 'HTTP_COOKIE' => 'rack.session=SESSION_TOKEN' | ||
expect(last_response).to be_ok | ||
end | ||
|
||
it 'denies requests with duplicate session cookies' do | ||
get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' | ||
expect(last_response).not_to be_ok | ||
end | ||
|
||
it 'denies requests with sneaky encoded session cookies' do | ||
get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.%73ession=SESSION_TOKEN' | ||
expect(last_response).not_to be_ok | ||
end | ||
|
||
it 'adds the correct Set-Cookie header' do | ||
get '/some/path', {}, 'HTTP_COOKIE' => 'rack.%73ession=EVIL_SESSION_TOKEN; rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' | ||
|
||
expected_header = <<-END.chomp | ||
rack.%2573ession=; domain=example.org; path=/; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
rack.%2573ession=; domain=example.org; path=/some; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
rack.%2573ession=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
rack.session=; domain=example.org; path=/; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
rack.session=; domain=example.org; path=/some; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
rack.session=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970 00:00:00 -0000 | ||
END | ||
expect(last_response.headers['Set-Cookie']).to eq(expected_header) | ||
end | ||
end | ||
|
||
context 'with redirect reaction' do | ||
before(:each) do | ||
mock_app do | ||
use Rack::Protection::CookieTossing, :reaction => :redirect | ||
run DummyApp | ||
end | ||
end | ||
|
||
it 'redirects requests with duplicate session cookies' do | ||
get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' | ||
expect(last_response).to be_redirect | ||
expect(last_response.location).to eq('/') | ||
end | ||
|
||
it 'redirects requests with sneaky encoded session cookies' do | ||
get '/path', {}, 'HTTP_COOKIE' => 'rack.%73ession=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' | ||
expect(last_response).to be_redirect | ||
expect(last_response.location).to eq('/path') | ||
end | ||
end | ||
|
||
context 'with custom session key' do | ||
it 'denies requests with duplicate session cookies' do | ||
mock_app do | ||
use Rack::Protection::CookieTossing, :session_key => '_session' | ||
run DummyApp | ||
end | ||
|
||
get '/', {}, 'HTTP_COOKIE' => '_session=EVIL_SESSION_TOKEN; _session=SESSION_TOKEN' | ||
expect(last_response).not_to be_ok | ||
end | ||
end | ||
end |