secure_headers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

@@ -0,0 +1,40 @@
1
+ module SecureHeaders
2
+ class XContentTypeOptionsBuildError < StandardError; end
3
+ # IE only
4
+ class XContentTypeOptions
5
+ module Constants
6
+ X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options"
7
+ DEFAULT_VALUE = "nosniff"
8
+ end
9
+ include Constants
10
+
11
+ def initialize(config=nil)
12
+ @config = config
13
+ validate_config unless @config.nil?
14
+ end
15
+
16
+ def name
17
+ X_CONTENT_TYPE_OPTIONS_HEADER_NAME
18
+ end
19
+
20
+ def value
21
+ case @config
22
+ when NilClass
23
+ DEFAULT_VALUE
24
+ when String
25
+ @config
26
+ else
27
+ @config[:value]
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def validate_config
34
+ value = @config.is_a?(Hash) ? @config[:value] : @config
35
+ unless value.casecmp(DEFAULT_VALUE) == 0
36
+ raise XContentTypeOptionsBuildError.new("Value can only be nil or 'nosniff'")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ module SecureHeaders
2
+ class XFOBuildError < StandardError; end
3
+ class XFrameOptions
4
+ module Constants
5
+ XFO_HEADER_NAME = "X-FRAME-OPTIONS"
6
+ DEFAULT_VALUE = 'SAMEORIGIN'
7
+ VALID_XFO_HEADER = /\A(SAMEORIGIN\z|DENY\z|ALLOW-FROM:)/i
8
+ end
9
+ include Constants
10
+
11
+ def initialize(config = nil)
12
+ @config = config
13
+ validate_config unless @config.nil?
14
+ end
15
+
16
+ def name
17
+ XFO_HEADER_NAME
18
+ end
19
+
20
+ def value
21
+ case @config
22
+ when NilClass
23
+ DEFAULT_VALUE
24
+ when String
25
+ @config
26
+ else
27
+ @config[:value]
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def validate_config
34
+ value = @config.is_a?(Hash) ? @config[:value] : @config
35
+ unless value =~ VALID_XFO_HEADER
36
+ raise XFOBuildError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,54 @@
1
+ module SecureHeaders
2
+ class XXssProtectionBuildError < StandardError; end
3
+ # IE only
4
+ class XXssProtection
5
+ module Constants
6
+ X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection'
7
+ DEFAULT_VALUE = "1"
8
+ VALID_X_XSS_HEADER = /\A[01](; mode=block)?\z/i
9
+ end
10
+ include Constants
11
+
12
+ def initialize(config=nil)
13
+ @config = config
14
+ validate_config unless @config.nil?
15
+ end
16
+
17
+ def name
18
+ X_XSS_PROTECTION_HEADER_NAME
19
+ end
20
+
21
+ def value
22
+ case @config
23
+ when NilClass
24
+ DEFAULT_VALUE
25
+ when String
26
+ @config
27
+ else
28
+ value = @config[:value].to_s
29
+ value += "; mode=#{@config[:mode]}" if @config[:mode]
30
+ value
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def validate_config
37
+ if @config.is_a? Hash
38
+ if !@config[:value]
39
+ raise XXssProtectionBuildError.new(":value key is missing")
40
+ elsif @config[:value]
41
+ unless [0,1].include?(@config[:value])
42
+ raise XXssProtectionBuildError.new(":value must be 1 or 0")
43
+ end
44
+
45
+ if @config[:mode] && @config[:mode].casecmp('block') != 0
46
+ raise XXssProtectionBuildError.new(":mode must nil or 'block'")
47
+ end
48
+ end
49
+ elsif @config.is_a? String
50
+ raise XXssProtectionBuildError.new("Invalid format (see VALID_X_XSS_HEADER)") unless @config =~ VALID_X_XSS_HEADER
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # rails 3.1+
2
+ if defined?(Rails::Railtie)
3
+ module SecureHeaders
4
+ class Railtie < Rails::Engine
5
+ isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0
6
+ ActionController::Base.send :include, ::SecureHeaders
7
+ end
8
+ end
9
+ else
10
+ module ActionController
11
+ class Base
12
+ include ::SecureHeaders
13
+ end
14
+ end
15
+
16
+ module SecureHeaders
17
+ module Routing
18
+ module MapperExtensions
19
+ def csp_endpoint
20
+ @set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ if defined?(ActiveSupport::Dependencies)
27
+ if ActiveSupport::Dependencies.autoload_paths
28
+ ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
29
+ else
30
+ ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
31
+ end
32
+ end
33
+
34
+ if defined? ActionController::Routing
35
+ ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module SecureHeaders
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'secure_headers/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "secure_headers"
8
+ gem.version = SecureHeaders::VERSION
9
+ gem.authors = ["Neil Matatall"]
10
+ gem.email = ["neil.matatall@gmail.com"]
11
+ gem.description = %q{Add easily configured browser headers to responses.}
12
+ gem.summary = %q{Add easily configured browser headers to responses
13
+ including content security policy, x-frame-options,
14
+ strict-transport-security and more.}
15
+ gem.homepage = "https://github.com/twitter/secureheaders"
16
+ gem.license = "Apache Public License 2.0"
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ["lib"]
21
+ gem.add_dependency "brwsr", ">= 1.1.1"
22
+ gem.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe ContentSecurityPolicyController do
4
+ let(:params) {
5
+ {
6
+ "csp-report" => {
7
+ "document-uri" => "http://localhost:3001/csp","violated-directive" => "script-src 'none'",
8
+ "original-policy" => "default-src https://* 'unsafe-eval'; frame-src 'self'; img-src chrome-extension: https://*; report-uri http://localhost:3001/scribes/csp_report; script-src 'none'; style-src 'unsafe-inline' 'self';",
9
+ "blocked-uri" => "http://localhost:3001/stuff.js"
10
+ }
11
+ }
12
+ }
13
+
14
+ describe "#csp" do
15
+ let(:request) { double().as_null_object }
16
+ let(:endpoint) { "https://example.com" }
17
+ let(:secondary_endpoint) { "https://internal.example.com" }
18
+
19
+ before(:each) do
20
+ SecureHeaders::Configuration.stub(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
21
+ subject.should_receive :head
22
+ subject.stub(:params).and_return(params)
23
+ Net::HTTP.any_instance.stub(:request)
24
+ end
25
+
26
+ context "delivery endpoint" do
27
+ it "posts over ssl" do
28
+ subject.should_receive(:use_ssl)
29
+ subject.scribe
30
+ end
31
+
32
+ it "posts over plain http" do
33
+ SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => 'http://example.com')
34
+ subject.should_not_receive(:use_ssl)
35
+ subject.scribe
36
+ end
37
+ end
38
+
39
+ it "makes a POST request" do
40
+ Net::HTTP.stub(:new).and_return(request)
41
+ request.should_receive(:request).with(instance_of(::Net::HTTP::Post))
42
+ params.stub(:to_json)
43
+ subject.scribe
44
+ end
45
+
46
+ it "POSTs to the configured forward_endpoint" do
47
+ Net::HTTP::Post.should_receive(:new).with(secondary_endpoint).and_return(request)
48
+ subject.scribe
49
+ end
50
+
51
+ it "does not POST if there is no forwarder configured" do
52
+ SecureHeaders::Configuration.stub(:csp).and_return({})
53
+ Net::HTTP::Post.should_not_receive(:new)
54
+ subject.scribe
55
+ end
56
+
57
+ it "eliminates known phony CSP reports" do
58
+ SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => nil)
59
+ Net::HTTP::Post.should_not_receive :new
60
+ subject.scribe
61
+ end
62
+
63
+ it "logs errors when it cannot forward the CSP report" do
64
+ class Rails; def logger; end; end
65
+ logger = double(:repond_to? => true)
66
+ Rails.stub(:logger).and_return(logger)
67
+
68
+ SecureHeaders::Configuration.stub(:csp).and_raise(StandardError)
69
+
70
+ logger.should_receive(:warn)
71
+ subject.scribe
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,382 @@
1
+ require 'spec_helper'
2
+ require 'brwsr'
3
+
4
+ module SecureHeaders
5
+ describe ContentSecurityPolicy do
6
+ let(:default_opts) do
7
+ {
8
+ :disable_chrome_extension => true,
9
+ :disable_fill_missing => true,
10
+ :default_src => 'https://*',
11
+ :report_uri => '/csp_report',
12
+ :script_src => 'inline eval https://* data:',
13
+ :style_src => "inline https://* chrome-extension: about:"
14
+ }
15
+ end
16
+
17
+ IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
18
+ FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8"
19
+ FIREFOX_18 = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/18.0 Firefox/18.0"
20
+ CHROME = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4"
21
+
22
+ def request_for user_agent, request_uri=nil, options={:ssl => false}
23
+ double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
24
+ end
25
+
26
+ before(:each) do
27
+ @options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
28
+ end
29
+
30
+ describe "#name" do
31
+ context "when in report-only mode" do
32
+ specify { ContentSecurityPolicy.new(request_for(IE), default_opts).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
33
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX), default_opts).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
34
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), default_opts).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
35
+ specify { ContentSecurityPolicy.new(request_for(CHROME), default_opts).name.should == WEBKIT_CSP_HEADER_NAME + "-Report-Only"}
36
+ end
37
+
38
+ context "when in enforce mode" do
39
+ let(:opts) { default_opts.merge(:enforce => true)}
40
+
41
+ specify { ContentSecurityPolicy.new(request_for(IE), opts).name.should == STANDARD_HEADER_NAME}
42
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX), opts).name.should == FIREFOX_CSP_HEADER_NAME}
43
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts).name.should == FIREFOX_CSP_HEADER_NAME}
44
+ specify { ContentSecurityPolicy.new(request_for(CHROME), opts).name.should == WEBKIT_CSP_HEADER_NAME}
45
+ end
46
+
47
+ context "when in experimental mode" do
48
+ let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})}
49
+ specify { ContentSecurityPolicy.new(request_for(IE), opts, :experimental => true).name.should == STANDARD_HEADER_NAME + "-Report-Only"}
50
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
51
+ specify { ContentSecurityPolicy.new(request_for(FIREFOX_18), opts, :experimental => true).name.should == FIREFOX_CSP_HEADER_NAME + "-Report-Only"}
52
+ specify { ContentSecurityPolicy.new(request_for(CHROME), opts, :experimental => true).name.should == WEBKIT_CSP_HEADER_NAME + "-Report-Only"}
53
+ end
54
+ end
55
+
56
+ describe "#normalize_csp_options" do
57
+ before(:each) do
58
+ default_opts.delete(:disable_chrome_extension)
59
+ default_opts.delete(:disable_fill_missing)
60
+ default_opts[:script_src] << ' self none'
61
+ @opts = default_opts
62
+ end
63
+
64
+ context "X-Content-Security-Policy" do
65
+ it "converts the script values to their equivilents" do
66
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), @opts)
67
+ csp.value.should include("script-src https://* data: 'self' 'none'")
68
+ csp.value.should include('options inline-script eval-script')
69
+ end
70
+ end
71
+
72
+ context "X-Webkit-CSP" do
73
+ it "converts the script values to their equivilents" do
74
+ csp = ContentSecurityPolicy.new(request_for(CHROME), @opts)
75
+ csp.value.should include("script-src 'unsafe-inline' 'unsafe-eval' https://* data: 'self' 'none' chrome-extension")
76
+ end
77
+ end
78
+ end
79
+
80
+ describe "#build_impl_specific_directives" do
81
+ context "X-Content-Security-Policy" do
82
+ it "moves script-src inline and eval values to the options directive" do
83
+ opts = {
84
+ :default_src => 'https://*',
85
+ :script_src => "inline eval https://*"
86
+ }
87
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
88
+ browser_specific = csp.send :build_impl_specific_directives
89
+ browser_specific.should include('options inline-script eval-script;')
90
+ end
91
+
92
+ it "does not move values from style-src into options" do
93
+ opts = {
94
+ :default_src => 'https://*',
95
+ :style_src => "inline eval https://*"
96
+ }
97
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
98
+ browser_specific = csp.send :build_impl_specific_directives
99
+ browser_specific.should_not include('inline-script')
100
+ browser_specific.should_not include('eval-script')
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "#supports_standard?" do
106
+ ['IE', 'Safari', 'Chrome'].each do |browser_name|
107
+ it "returns true for #{browser_name}" do
108
+ browser = Brwsr::Browser.new(:ua => browser_name)
109
+ subject.stub(:browser).and_return(browser)
110
+ subject.send(:supports_standard?).should be_true
111
+ end
112
+ end
113
+
114
+ it "returns true for Firefox v >= 18" do
115
+ browser = Brwsr::Browser.new(:ua => "Firefox 18")
116
+ subject.stub(:browser).and_return(browser)
117
+ subject.send(:supports_standard?).should be_true
118
+ end
119
+
120
+ it "returns false for Firefox v < 18" do
121
+ browser = Brwsr::Browser.new(:ua => "Firefox 17")
122
+ subject.stub(:browser).and_return(browser)
123
+ subject.send(:supports_standard?).should be_false
124
+ end
125
+ end
126
+
127
+ describe "#same_origin?" do
128
+ let(:origin) {"https://example.com:123"}
129
+
130
+ it "matches when host, scheme, and port match" do
131
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com"), {:report_uri => 'https://example.com'})
132
+ csp.send(:same_origin?).should be_true
133
+
134
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com:443"), {:report_uri => 'https://example.com'})
135
+ csp.send(:same_origin?).should be_true
136
+
137
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://example.com:123"), {:report_uri => 'https://example.com:123'})
138
+ csp.send(:same_origin?).should be_true
139
+
140
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://example.com'})
141
+ csp.send(:same_origin?).should be_true
142
+
143
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://example.com:80'})
144
+ csp.send(:same_origin?).should be_true
145
+
146
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com:80"), {:report_uri => 'http://example.com'})
147
+ csp.send(:same_origin?).should be_true
148
+ end
149
+
150
+ it "does not match port mismatches" do
151
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com:81"), {:report_uri => 'http://example.com'})
152
+ csp.send(:same_origin?).should be_false
153
+ end
154
+
155
+ it "does not match host mismatches" do
156
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://example.com"), {:report_uri => 'http://twitter.com'})
157
+ csp.send(:same_origin?).should be_false
158
+ end
159
+
160
+ it "does not match host mismatches because of subdomains" do
161
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "http://sub.example.com"), {:report_uri => 'http://example.com'})
162
+ csp.send(:same_origin?).should be_false
163
+ end
164
+
165
+ it "does not match scheme mismatches" do
166
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "ftp://example.com"), {:report_uri => 'https://example.com'})
167
+ csp.send(:same_origin?).should be_false
168
+ end
169
+
170
+ it "does not match on substring collisions" do
171
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, "https://anotherexample.com"), {:report_uri => 'https://example.com'})
172
+ csp.send(:same_origin?).should be_false
173
+ end
174
+ end
175
+
176
+ describe "#normalize_reporting_endpoint" do
177
+ let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
178
+
179
+ context "when using firefox" do
180
+ it "updates the report-uri when posting to a different host" do
181
+ subject.configure(request_for(FIREFOX, "https://anexample.com"), opts)
182
+ subject.report_uri.should == FF_CSP_ENDPOINT
183
+ end
184
+
185
+ it "updates the report-uri when posting to a different host for Firefox >= 18" do
186
+ subject.configure(request_for(FIREFOX, "https://anexample.com"), opts)
187
+ subject.report_uri.should == FF_CSP_ENDPOINT
188
+ end
189
+
190
+ it "doesn't set report-uri if no forward_endpoint is supplied" do
191
+ subject.configure(request_for(FIREFOX, "https://example.com"), :report_uri => "https://another.example.com")
192
+ subject.report_uri.should be_nil
193
+ end
194
+ end
195
+
196
+ it "does not update the URI is the report_uri is on the same origin" do
197
+ opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
198
+ subject.configure(request_for(FIREFOX, "https://example.com/somewhere"), opts)
199
+ subject.report_uri.should == 'https://example.com/csp'
200
+ end
201
+
202
+ it "does not update the report-uri when using a non-firefox browser" do
203
+ subject.configure(request_for(CHROME), opts)
204
+ subject.report_uri.should == 'https://example.com/csp'
205
+ end
206
+ end
207
+
208
+ describe "#value" do
209
+ it "raises an exception when default-src is missing" do
210
+ subject.configure(request_for(CHROME), {:script_src => 'anything'})
211
+ lambda {
212
+ subject.value
213
+ }.should raise_error(ContentSecurityPolicyBuildError, "Couldn't build CSP header :( Expected to find default_src directive value")
214
+ end
215
+
216
+ context "auto-whitelists data: uris for img-src" do
217
+
218
+ it "sets the value if no img-src specified" do
219
+ csp = ContentSecurityPolicy.new(request_for(CHROME), {:default_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true})
220
+ csp.value.should == "default-src 'self'; img-src data:;"
221
+ end
222
+
223
+ it "appends the value if img-src is specified" do
224
+ csp = ContentSecurityPolicy.new(request_for(CHROME), {:default_src => 'self', :img_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true})
225
+ csp.value.should == "default-src 'self'; img-src 'self' data:;"
226
+ end
227
+ end
228
+
229
+ it "fills in directives without values with default-src value" do
230
+ options = default_opts.merge(:disable_fill_missing => false)
231
+ csp = ContentSecurityPolicy.new(request_for(CHROME), options)
232
+ value = "default-src https://*; connect-src https://*; font-src https://*; frame-src https://*; img-src https://* data:; media-src https://*; object-src https://*; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;"
233
+ csp.value.should == value
234
+ end
235
+
236
+ it "sends the chrome csp header if an unknown browser is supplied" do
237
+ csp = ContentSecurityPolicy.new(request_for(IE), default_opts)
238
+ csp.value.should match "default-src"
239
+ end
240
+
241
+ context "X-Content-Security-Policy" do
242
+ it "builds a csp header for firefox" do
243
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), default_opts)
244
+ csp.value.should == "allow https://*; options inline-script eval-script; img-src data:; script-src https://* data:; style-src https://* chrome-extension: about:;"
245
+ end
246
+
247
+ it "copies connect-src values to xhr_src values" do
248
+ opts = {
249
+ :default_src => 'http://twitter.com',
250
+ :connect_src => 'self http://*.localhost.com:*',
251
+ :disable_chrome_extension => true,
252
+ :disable_fill_missing => true
253
+ }
254
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), opts)
255
+ csp.value.should =~ /xhr-src 'self' http:/
256
+ end
257
+
258
+ it "copies connect-src values to xhr_src values for FF 18" do
259
+ opts = {
260
+ :default_src => 'http://twitter.com',
261
+ :connect_src => 'self http://*.localhost.com:*',
262
+ :disable_chrome_extension => true,
263
+ :disable_fill_missing => true
264
+ }
265
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX_18), opts)
266
+ csp.value.should =~ /xhr-src 'self' http:\/\/\*\.localhost\.com:\*/
267
+ end
268
+
269
+ it "builds a w3c-style-ish header for Firefox > version 18" do
270
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX_18), default_opts)
271
+ csp.value.should =~ /default-src/
272
+ end
273
+ end
274
+
275
+ context "X-Webkit-CSP" do
276
+ it "builds a csp header for chrome" do
277
+ csp = ContentSecurityPolicy.new(request_for(CHROME), default_opts)
278
+ csp.value.should == "default-src https://*; img-src data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;"
279
+ end
280
+
281
+ it "ignores :forward_endpoint settings" do
282
+ csp = ContentSecurityPolicy.new(request_for(CHROME), @options_with_forwarding)
283
+ csp.value.should =~ /report-uri #{@options_with_forwarding[:report_uri]};/
284
+ end
285
+
286
+ it "whitelists chrome_extensions by default" do
287
+ opts = {
288
+ :default_src => 'https://*',
289
+ :report_uri => '/csp_report',
290
+ :script_src => 'inline eval https://* data:',
291
+ :style_src => "inline https://* chrome-extension: about:"
292
+ }
293
+
294
+ csp = ContentSecurityPolicy.new(request_for(CHROME), opts)
295
+
296
+ # ignore the report-uri directive
297
+ csp.value.split(';')[0...-1].each{|directive| directive.should =~ /chrome-extension:/}
298
+ end
299
+ end
300
+
301
+ context "when supplying a experimental values" do
302
+ let(:options) {{
303
+ :disable_chrome_extension => true,
304
+ :disable_fill_missing => true,
305
+ :default_src => 'self',
306
+ :script_src => 'https://*',
307
+ :experimental => {
308
+ :script_src => 'self'
309
+ }
310
+ }}
311
+
312
+ let(:header) {}
313
+ it "returns the original value" do
314
+ header = ContentSecurityPolicy.new(request_for(CHROME), options)
315
+ header.value.should == "default-src 'self'; img-src data:; script-src https://*;"
316
+ end
317
+
318
+ it "it returns the experimental value if requested" do
319
+ header = ContentSecurityPolicy.new(request_for(CHROME), options, :experimental => true)
320
+ header.value.should_not =~ /https/
321
+ end
322
+ end
323
+
324
+ context "when supplying additional http directive values" do
325
+ let(:options) {
326
+ default_opts.merge({
327
+ :http_additions => {
328
+ :frame_src => "http://*",
329
+ :img_src => "http://*"
330
+ }
331
+ })
332
+ }
333
+
334
+ it "adds directive values for headers on http" do
335
+ csp = ContentSecurityPolicy.new(request_for(CHROME), options)
336
+ csp.value.should == "default-src https://*; frame-src http://*; img-src http://* data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* chrome-extension: about:; report-uri /csp_report;"
337
+ end
338
+
339
+ it "does not add the directive values if requesting https" do
340
+ csp = ContentSecurityPolicy.new(request_for(CHROME, '/', :ssl => true), options)
341
+ csp.value.should_not =~ /http:/
342
+ end
343
+
344
+ context "when supplying an experimental block" do
345
+ # this simulates the situation where we are enforcing that scripts
346
+ # only come from http[s]? depending if we're on ssl or not. The
347
+ # report only tag will allow scripts from self over ssl, and
348
+ # from a secure CDN over non-ssl
349
+ let(:options) {{
350
+ :disable_chrome_extension => true,
351
+ :disable_fill_missing => true,
352
+ :default_src => 'self',
353
+ :script_src => 'https://*',
354
+ :http_additions => {
355
+ :script_src => 'http://*'
356
+ },
357
+ :experimental => {
358
+ :script_src => 'self',
359
+ :http_additions => {
360
+ :script_src => 'https://mycdn.example.com'
361
+ }
362
+ }
363
+ }}
364
+ # for comparison purposes, if not using the experimental header this would produce
365
+ # "allow 'self'; script-src https://*" for https requests
366
+ # and
367
+ # "allow 'self; script-src https://* http://*" for http requests
368
+
369
+ it "uses the value in the experimental block over SSL" do
370
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX, '/', :ssl => true), options, :experimental => true)
371
+ csp.value.should == "allow 'self'; img-src data:; script-src 'self';"
372
+ end
373
+
374
+ it "merges the values from experimental/http_additions when not over SSL" do
375
+ csp = ContentSecurityPolicy.new(request_for(FIREFOX), options, :experimental => true)
376
+ csp.value.should == "allow 'self'; img-src data:; script-src 'self' https://mycdn.example.com;"
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end