diff --git a/rack-protection/README.md b/rack-protection/README.md index a0e24d2b..bf6e5c6f 100644 --- a/rack-protection/README.md +++ b/rack-protection/README.md @@ -79,6 +79,12 @@ Prevented by: * `Rack::Protection::IPSpoofing` +## Helps to protect against protocol downgrade attacks and cookie hijacking + +Prevented by: + +* `Rack::Protection::StrictTransport` (not included by `use Rack::Protection`) + # Installation gem install rack-protection diff --git a/rack-protection/lib/rack/protection.rb b/rack-protection/lib/rack/protection.rb index 6b8ab9d0..23424a0a 100644 --- a/rack-protection/lib/rack/protection.rb +++ b/rack-protection/lib/rack/protection.rb @@ -16,6 +16,7 @@ module Rack autoload :RemoteReferrer, 'rack/protection/remote_referrer' autoload :RemoteToken, 'rack/protection/remote_token' autoload :SessionHijacking, 'rack/protection/session_hijacking' + autoload :StrictTransport, 'rack/protection/strict_transport' autoload :XSSHeader, 'rack/protection/xss_header' def self.new(app, options = {}) @@ -26,6 +27,7 @@ module Rack use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token use ::Rack::Protection::FormToken, options if use_these.include? :form_token + use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport use ::Rack::Protection::ContentSecurityPolicy, options unless except.include? :content_security_policy use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin diff --git a/rack-protection/lib/rack/protection/strict_transport.rb b/rack-protection/lib/rack/protection/strict_transport.rb new file mode 100644 index 00000000..fd54cfbf --- /dev/null +++ b/rack-protection/lib/rack/protection/strict_transport.rb @@ -0,0 +1,37 @@ +require 'rack/protection' + +module Rack + module Protection + ## + # Prevented attack:: Protects against against protocol downgrade attacks and cookie hijacking. + # Supported browsers:: all + # More infos:: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security + # + # browser will prevent any communications from being sent over HTTP + # to the specified domain and will instead send all communications over HTTPS. + # It also prevents HTTPS click through prompts on browsers. + # + # Options: + # + # max_age:: How long future requests to the domain should go over HTTPS; specified in seconds + # include_subdomains:: If all present and future subdomains will be HTTPS + + class StrictTransport < Base + default_options :max_age => 31_536_000, :include_subdomains => false + + def strict_transport + @strict_transport ||= begin + strict_transport = 'max-age=' + options[:max_age].to_s + strict_transport += '; includeSubDomains' if options[:include_subdomains] + strict_transport.to_str + end + end + + def call(env) + status, headers, body = @app.call(env) + headers['Strict-Transport-Security'] ||= strict_transport + [status, headers, body] + end + end + end +end diff --git a/rack-protection/spec/lib/rack/protection/strict_transport_spec.rb b/rack-protection/spec/lib/rack/protection/strict_transport_spec.rb new file mode 100644 index 00000000..dbb4fde8 --- /dev/null +++ b/rack-protection/spec/lib/rack/protection/strict_transport_spec.rb @@ -0,0 +1,25 @@ +describe Rack::Protection::StrictTransport do + it_behaves_like "any rack application" + + it 'should set the Strict-Transport-Security header' do + expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000") + end + + it 'should allow changing the max-age option' do + mock_app do + use Rack::Protection::StrictTransport, :max_age => 16_070_400 + run DummyApp + end + + expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=16070400") + end + + it 'should allow switching on the include_subdomains option' do + mock_app do + use Rack::Protection::StrictTransport, :include_subdomains => true + run DummyApp + end + + expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000; includeSubDomains") + end +end