diff --git a/rack-protection/README.md b/rack-protection/README.md index 5da6d6ab..a0e24d2b 100644 --- a/rack-protection/README.md +++ b/rack-protection/README.md @@ -53,6 +53,7 @@ Prevented by: * `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`) * `Rack::Protection::XSSHeader` (Internet Explorer only) +* `Rack::Protection::ContentSecurityPolicy` ## Clickjacking diff --git a/rack-protection/lib/rack/protection.rb b/rack-protection/lib/rack/protection.rb index 604e749c..6b8ab9d0 100644 --- a/rack-protection/lib/rack/protection.rb +++ b/rack-protection/lib/rack/protection.rb @@ -3,36 +3,38 @@ require 'rack' module Rack module Protection - autoload :AuthenticityToken, 'rack/protection/authenticity_token' - autoload :Base, 'rack/protection/base' - autoload :EscapedParams, 'rack/protection/escaped_params' - autoload :FormToken, 'rack/protection/form_token' - autoload :FrameOptions, 'rack/protection/frame_options' - autoload :HttpOrigin, 'rack/protection/http_origin' - autoload :IPSpoofing, 'rack/protection/ip_spoofing' - autoload :JsonCsrf, 'rack/protection/json_csrf' - autoload :PathTraversal, 'rack/protection/path_traversal' - autoload :RemoteReferrer, 'rack/protection/remote_referrer' - autoload :RemoteToken, 'rack/protection/remote_token' - autoload :SessionHijacking, 'rack/protection/session_hijacking' - autoload :XSSHeader, 'rack/protection/xss_header' + autoload :AuthenticityToken, 'rack/protection/authenticity_token' + autoload :Base, 'rack/protection/base' + autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy' + autoload :EscapedParams, 'rack/protection/escaped_params' + autoload :FormToken, 'rack/protection/form_token' + autoload :FrameOptions, 'rack/protection/frame_options' + autoload :HttpOrigin, 'rack/protection/http_origin' + autoload :IPSpoofing, 'rack/protection/ip_spoofing' + autoload :JsonCsrf, 'rack/protection/json_csrf' + autoload :PathTraversal, 'rack/protection/path_traversal' + autoload :RemoteReferrer, 'rack/protection/remote_referrer' + autoload :RemoteToken, 'rack/protection/remote_token' + autoload :SessionHijacking, 'rack/protection/session_hijacking' + autoload :XSSHeader, 'rack/protection/xss_header' def self.new(app, options = {}) # does not include: RemoteReferrer, AuthenticityToken and FormToken except = Array options[:except] use_these = Array options[:use] Rack::Builder.new do - 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::FrameOptions, options unless except.include? :frame_options - use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin - use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing - use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf - use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal - use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token - use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking - use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header + 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::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 + use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing + use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf + use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal + use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token + use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking + use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header run app end.to_app end diff --git a/rack-protection/lib/rack/protection/content_security_policy.rb b/rack-protection/lib/rack/protection/content_security_policy.rb new file mode 100644 index 00000000..fc53e33b --- /dev/null +++ b/rack-protection/lib/rack/protection/content_security_policy.rb @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +require 'rack/protection' + +module Rack + module Protection + ## + # Prevented attack:: XSS and others + # Supported browsers:: Firefox 23+, Safari 7+, Chrome 25+, Opera 15+ + # http://caniuse.com/contentsecuritypolicy + # More infos:: http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + # http://content-security-policy.com/ + # + # Sets Content-Security-Policy-Report(-Only) header to tell the browser what resource are allowed to load from which domain. + # + # Options: + # (descriptions taken from http://www.html5rocks.com/en/tutorials/security/content-security-policy/) + # + # connect_src:: limits the origins to which you can connect (via XHR, + # WebSockets, and EventSource). + # + # font_src:: specifies the origins that can serve web fonts. Google’s + # Web Fonts could be enabled via font-src + # https://themes.googleusercontent.com + # + # frame_src:: lists the origins that can be embedded as frames. For + # example: frame-src https://youtube.com would enable + # embedding YouTube videos, but no other origins. + # + # img_src:: defines the origins from which images can be loaded. + # + # media_src:: restricts the origins allowed to deliver video and audio. + # + # object_src:: allows control over Flash and other plugins. + # + # style_src:: is script-src’s counterpart for stylesheets. + # + # report_uri:: instruct the browser to POST JSON-formatted violation + # reports to a location specified in a report-uri directive. + # + # report_only:: ask the browser to monitor a policy, reporting violations, + # but not enforcing the restrictions. + # + # sandbox:: if the sandbox directive is present, the page will be + # treated as though it was loaded inside of an iframe with + # a sandbox attribute. + class ContentSecurityPolicy < Base + default_options :default_src => :none, :script_src => :self, :img_src => :self, :style_src => :self, :connect_src => :self, :report_only => false + + KEYS = [:default_src, :script_src, :connect_src, :font_src, :frame_src, :media_src, :style_src, :object_src, :report_uri, :sandbox] + + def collect_options + KEYS.collect do |k| + "#{k.to_s.sub(/_/, '-')} #{options[k]}" if options.key?(k) + end.compact.join('; ') + end + + def call(env) + status, headers, body = @app.call(env) + header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' + headers[header] ||= collect_options if html? headers + [status, headers, body] + end + end + end +end diff --git a/rack-protection/spec/content_security_policy_spec.rb b/rack-protection/spec/content_security_policy_spec.rb new file mode 100644 index 00000000..491c8992 --- /dev/null +++ b/rack-protection/spec/content_security_policy_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../spec_helper.rb', __FILE__) + +describe Rack::Protection::ContentSecurityPolicy do + it_behaves_like "any rack application" + + it 'should set the Content Security Policy' do + get('/', {}, 'wants' => 'text/html').headers["Content-Security-Policy"].should == "default-src none; script-src self; connect-src self; style-src self" + end + + it 'should not set the Content Security Policy for other content types' do + headers = get('/', {}, 'wants' => 'text/foo').headers + headers["Content-Security-Policy"].should be_nil + headers["Content-Security-Policy-Report-Only"].should be_nil + end + + it 'should allow changing the protection settings' do + mock_app do + use Rack::Protection::ContentSecurityPolicy, :default_src => 'none', :script_src => 'https://cdn.mybank.net', :style_src => 'https://cdn.mybank.net', :img_src => 'https://cdn.mybank.net', :connect_src => 'https://api.mybank.com', :frame_src => 'self', :font_src => 'https://cdn.mybank.net', :object_src => 'https://cdn.mybank.net', :media_src => 'https://cdn.mybank.net', :report_uri => '/my_amazing_csp_report_parser', :sandbox => 'allow-scripts' + + run DummyApp + end + + headers = get('/', {}, 'wants' => 'text/html').headers + headers["Content-Security-Policy"].should == "default-src none; script-src https://cdn.mybank.net; connect-src https://api.mybank.com; font-src https://cdn.mybank.net; frame-src self; media-src https://cdn.mybank.net; style-src https://cdn.mybank.net; object-src https://cdn.mybank.net; report-uri /my_amazing_csp_report_parser; sandbox allow-scripts" + headers["Content-Security-Policy-Report-Only"].should be_nil + end + + it 'should allow changing report only' do + # I have no clue what other modes are available + mock_app do + use Rack::Protection::ContentSecurityPolicy, :report_uri => '/my_amazing_csp_report_parser', :report_only => true + run DummyApp + end + + headers = get('/', {}, 'wants' => 'text/html').headers + headers["Content-Security-Policy"].should be_nil + headers["Content-Security-Policy-Report-Only"].should == "default-src none; script-src self; connect-src self; style-src self; report-uri /my_amazing_csp_report_parser" + end + + it 'should not override the header if already set' do + mock_app with_headers("Content-Security-Policy" => "default-src: none") + get('/', {}, 'wants' => 'text/html').headers["Content-Security-Policy"].should == "default-src: none" + end +end