secure_headers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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