Add skip_before_filter functionality to each header
This commit is contained in:
Родитель
214781102b
Коммит
618781602c
14
README.md
14
README.md
|
@ -37,13 +37,25 @@ Functionality provided
|
|||
|
||||
By default, it will set all of the headers listed in the options section below unless specified.
|
||||
|
||||
### Disabling
|
||||
|
||||
Use the standard `skip_before_filter :filter_name, options` mechanism. e.g. `skip_before_filter :set_csp_header, :only => :tinymce_page`
|
||||
|
||||
The following methods are going to be called, unles they are provided in a `skip_before_filter` block.
|
||||
|
||||
* `:set_csp_header`
|
||||
* `:set_hsts_header`
|
||||
* `:set_x_frame_options_header`
|
||||
* `:set_x_xss_protection_header`
|
||||
* `:set_x_content_type_options_header`
|
||||
|
||||
### Automagic
|
||||
|
||||
This gem makes a few assumptions about how you will use some features. For example:
|
||||
|
||||
* It adds 'chrome-extension:' to your CSP directives by default. This helps drastically reduce the amount of reports, but you can also disable this feature by supplying :disable_chrome_extension => true.
|
||||
* It fills any blank directives with the value in :default_src Getting a default\-src report is pretty useless. This way, you will always know what type of violation occurred. You can disable this feature by supplying :disable_fill_missing => true.
|
||||
* It copies the connect\-src value to xhr\-src for AJAX requests.
|
||||
* It copies the connect\-src value to xhr\-src for AJAX requests when using Firefox.
|
||||
* Firefox does not support cross\-origin CSP reports. If we are using Firefox, AND the value for :report_uri does not satisfy the same\-origin requirements, we will instead forward to an internal endpoint (`FF_CSP_ENDPOINT`). This is also the case if :report_uri only contains a path, which we assume will be cross host. This endpoint will in turn forward the request to the value in :forward_endpoint without restriction. More information can be found in the "Note on Firefox handling of CSP" section.
|
||||
|
||||
|
||||
|
|
|
@ -33,7 +33,11 @@ module SecureHeaders
|
|||
|
||||
def ensure_security_headers options = {}
|
||||
self.secure_headers_options = options
|
||||
before_filter :set_security_headers
|
||||
before_filter :set_hsts_header
|
||||
before_filter :set_x_frame_options_header
|
||||
before_filter :set_csp_header
|
||||
before_filter :set_x_xss_protection_header
|
||||
before_filter :set_x_content_type_options_header
|
||||
end
|
||||
|
||||
# we can't use ||= because I'm overloading false => disable, nil => default
|
||||
|
@ -44,18 +48,13 @@ module SecureHeaders
|
|||
end
|
||||
|
||||
module InstanceMethods
|
||||
def set_security_headers(options = self.class.secure_headers_options)
|
||||
brwsr = Brwsr::Browser.new(:ua => request.env['HTTP_USER_AGENT'])
|
||||
set_hsts_header(options[:hsts]) if request.ssl?
|
||||
set_x_frame_options_header(options[:x_frame_options])
|
||||
set_csp_header(request, options[:csp]) unless broken_implementation?(brwsr)
|
||||
set_x_xss_protection_header(options[:x_xss_protection])
|
||||
if brwsr.ie?
|
||||
set_x_content_type_options_header(options[:x_content_type_options])
|
||||
end
|
||||
def brwsr
|
||||
@secure_headers_brwsr ||= Brwsr::Browser.new(:ua => request.env['HTTP_USER_AGENT'])
|
||||
end
|
||||
|
||||
def set_csp_header(request, options=nil)
|
||||
def set_csp_header(options=self.class.secure_headers_options[:csp])
|
||||
return if broken_implementation?(brwsr)
|
||||
|
||||
options = self.class.options_for :csp, options
|
||||
return if options == false
|
||||
|
||||
|
@ -67,6 +66,26 @@ module SecureHeaders
|
|||
end
|
||||
end
|
||||
|
||||
def set_x_frame_options_header(options=self.class.secure_headers_options[:x_frame_options])
|
||||
set_a_header(:x_frame_options, XFrameOptions, options)
|
||||
end
|
||||
|
||||
def set_x_content_type_options_header(options=self.class.secure_headers_options[:x_content_type_options])
|
||||
return unless brwsr.ie?
|
||||
set_a_header(:x_content_type_options, XContentTypeOptions, options)
|
||||
end
|
||||
|
||||
def set_x_xss_protection_header(options=self.class.secure_headers_options[:x_xss_protection])
|
||||
set_a_header(:x_xss_protection, XXssProtection, options)
|
||||
end
|
||||
|
||||
def set_hsts_header(options=self.class.secure_headers_options[:hsts])
|
||||
return unless request.ssl?
|
||||
set_a_header(:hsts, StrictTransportSecurity, options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_a_header(name, klass, options=nil)
|
||||
options = self.class.options_for name, options
|
||||
return if options == false
|
||||
|
@ -75,28 +94,10 @@ module SecureHeaders
|
|||
set_header(header.name, header.value)
|
||||
end
|
||||
|
||||
def set_x_frame_options_header(options=nil)
|
||||
set_a_header(:x_frame_options, XFrameOptions, options)
|
||||
end
|
||||
|
||||
def set_x_content_type_options_header(options=nil)
|
||||
set_a_header(:x_content_type_options, XContentTypeOptions, options)
|
||||
end
|
||||
|
||||
def set_x_xss_protection_header(options=nil)
|
||||
set_a_header(:x_xss_protection, XXssProtection, options)
|
||||
end
|
||||
|
||||
def set_hsts_header(options=nil)
|
||||
set_a_header(:hsts, StrictTransportSecurity, options)
|
||||
end
|
||||
|
||||
def set_header(name, value)
|
||||
response.headers[name] = value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def broken_implementation?(browser)
|
||||
#IOS 5 sometimes refuses to load external resources even when whitelisted with CSP
|
||||
return browser.ios5?
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
# half spec test, half integration test...
|
||||
describe SecureHeaders do
|
||||
class DummyClass
|
||||
include ::SecureHeaders
|
||||
|
@ -28,13 +27,6 @@ describe SecureHeaders do
|
|||
:ios5 => "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"
|
||||
}
|
||||
|
||||
describe "#set_header" do
|
||||
it "sets the given header and value" do
|
||||
headers.should_receive(:[]=).with("header", "value")
|
||||
subject.set_header("header", "value")
|
||||
end
|
||||
end
|
||||
|
||||
def should_assign_header name, value
|
||||
subject.should_receive(:set_header).with(name, value)
|
||||
end
|
||||
|
@ -61,10 +53,18 @@ describe SecureHeaders do
|
|||
end
|
||||
end
|
||||
|
||||
def set_security_headers(subject)
|
||||
subject.set_csp_header
|
||||
subject.set_hsts_header
|
||||
subject.set_x_frame_options_header
|
||||
subject.set_x_content_type_options_header
|
||||
subject.set_x_xss_protection_header
|
||||
end
|
||||
|
||||
describe "#ensure_security_headers" do
|
||||
it "sets a before filter" do
|
||||
options = {}
|
||||
DummyClass.should_receive(:before_filter).with(:set_security_headers)
|
||||
DummyClass.should_receive(:before_filter).exactly(5).times
|
||||
DummyClass.ensure_security_headers(options)
|
||||
end
|
||||
end
|
||||
|
@ -88,50 +88,54 @@ describe SecureHeaders do
|
|||
end
|
||||
|
||||
subject.should_receive(:set_header).exactly(number_of_headers).times # a request for a given header
|
||||
subject.set_security_headers
|
||||
subject.set_csp_header
|
||||
subject.set_x_frame_options_header
|
||||
subject.set_hsts_header
|
||||
subject.set_x_xss_protection_header
|
||||
subject.set_x_content_type_options_header
|
||||
end
|
||||
end
|
||||
|
||||
it "does not set the X-Content-Type-Options when disabled" do
|
||||
stub_user_agent(USER_AGENTS[:ie])
|
||||
should_not_assign_header(X_CONTENT_TYPE_OPTIONS_HEADER_NAME)
|
||||
subject.set_security_headers(:x_content_type_options => false)
|
||||
subject.set_x_content_type_options_header(false)
|
||||
end
|
||||
|
||||
it "does not set the X-XSS-PROTECTION when disabled" do
|
||||
stub_user_agent(USER_AGENTS[:ie])
|
||||
should_not_assign_header(X_XSS_PROTECTION_HEADER_NAME)
|
||||
subject.set_security_headers(:x_xss_protection => false)
|
||||
subject.set_x_xss_protection_header(false)
|
||||
end
|
||||
|
||||
it "does not set the X-FRAME-OPTIONS header if disabled" do
|
||||
should_not_assign_header(XFO_HEADER_NAME)
|
||||
subject.set_security_headers(:x_frame_options => false)
|
||||
subject.set_x_frame_options_header(false)
|
||||
end
|
||||
|
||||
it "does not set the hsts header if disabled" do
|
||||
should_not_assign_header(HSTS_HEADER_NAME)
|
||||
subject.set_security_headers(:hsts => false)
|
||||
subject.set_hsts_header(false)
|
||||
end
|
||||
|
||||
it "does not set the hsts header the request is over HTTP" do
|
||||
subject.stub_chain(:request, :ssl?).and_return(false)
|
||||
should_not_assign_header(HSTS_HEADER_NAME)
|
||||
subject.set_security_headers(:hsts => {:include_subdomains => true})
|
||||
subject.set_hsts_header({:include_subdomains => true})
|
||||
end
|
||||
|
||||
it "does not set the CSP header if disabled" do
|
||||
stub_user_agent(USER_AGENTS[:chrome])
|
||||
should_not_assign_header(WEBKIT_CSP_HEADER_NAME)
|
||||
subject.set_security_headers(options_for(:csp).merge(:csp => false))
|
||||
subject.set_csp_header(options_for(:csp).merge(:csp => false))
|
||||
end
|
||||
|
||||
# apparently iOS5 safari with CSP in enforce mode causes nothing to render
|
||||
# it has no effect in report-only mode (as in no report is sent)
|
||||
it "does not set CSP header if using ios5" do
|
||||
stub_user_agent(USER_AGENTS[:ios5])
|
||||
subject.should_not_receive(:set_csp_header)
|
||||
subject.set_security_headers(options_for(:csp))
|
||||
subject.should_not_receive(:set_header)
|
||||
subject.set_csp_header(options_for(:csp))
|
||||
end
|
||||
|
||||
context "when disabled by configuration settings" do
|
||||
|
@ -144,7 +148,7 @@ describe SecureHeaders do
|
|||
config.csp = false
|
||||
end
|
||||
subject.should_not_receive(:set_header)
|
||||
subject.set_security_headers
|
||||
set_security_headers(subject)
|
||||
reset_config
|
||||
end
|
||||
end
|
||||
|
@ -202,7 +206,7 @@ describe SecureHeaders do
|
|||
it "sets CSP headers" do
|
||||
stub_user_agent(USER_AGENTS[:firefox])
|
||||
should_assign_header(FIREFOX_CSP_HEADER_NAME + "-Report-Only", FIREFOX_CSP_HEADER)
|
||||
subject.set_csp_header request
|
||||
subject.set_csp_header
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -210,7 +214,7 @@ describe SecureHeaders do
|
|||
it "sets default CSP header" do
|
||||
stub_user_agent(USER_AGENTS[:chrome])
|
||||
should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", WEBKIT_CSP_HEADER)
|
||||
subject.set_csp_header request
|
||||
subject.set_csp_header
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -218,7 +222,7 @@ describe SecureHeaders do
|
|||
it "sets the CSP header" do
|
||||
stub_user_agent(USER_AGENTS[:opera])
|
||||
should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", WEBKIT_CSP_HEADER)
|
||||
subject.set_csp_header request
|
||||
subject.set_csp_header
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -239,13 +243,13 @@ describe SecureHeaders do
|
|||
opts = @opts.merge(:enforce => false)
|
||||
should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", anything)
|
||||
should_not_assign_header(WEBKIT_CSP_HEADER_NAME)
|
||||
subject.set_csp_header request, opts
|
||||
subject.set_csp_header opts
|
||||
end
|
||||
|
||||
it "sets a header in enforce mode as well as report-only mode" do
|
||||
should_assign_header(WEBKIT_CSP_HEADER_NAME, anything)
|
||||
should_assign_header(WEBKIT_CSP_HEADER_NAME + "-Report-Only", anything)
|
||||
subject.set_csp_header request, @opts
|
||||
subject.set_csp_header @opts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Загрузка…
Ссылка в новой задаче