secure_headers 2.2.4 → 3.0.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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG.md +205 -0
  6. data/Gemfile +9 -10
  7. data/Guardfile +12 -0
  8. data/README.md +172 -274
  9. data/Rakefile +2 -36
  10. data/lib/secure_headers/configuration.rb +189 -0
  11. data/lib/secure_headers/headers/content_security_policy.rb +362 -211
  12. data/lib/secure_headers/headers/public_key_pins.rb +43 -57
  13. data/lib/secure_headers/headers/strict_transport_security.rb +21 -47
  14. data/lib/secure_headers/headers/x_content_type_options.rb +19 -32
  15. data/lib/secure_headers/headers/x_download_options.rb +18 -31
  16. data/lib/secure_headers/headers/x_frame_options.rb +24 -32
  17. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -32
  18. data/lib/secure_headers/headers/x_xss_protection.rb +17 -46
  19. data/lib/secure_headers/middleware.rb +15 -0
  20. data/lib/secure_headers/padrino.rb +1 -2
  21. data/lib/secure_headers/railtie.rb +9 -6
  22. data/lib/secure_headers/view_helper.rb +27 -44
  23. data/lib/secure_headers.rb +245 -150
  24. data/secure_headers.gemspec +7 -11
  25. data/spec/lib/secure_headers/configuration_spec.rb +80 -0
  26. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +175 -262
  27. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
  28. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
  29. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +13 -18
  30. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
  31. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
  32. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
  33. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +22 -30
  34. data/spec/lib/secure_headers/middleware_spec.rb +40 -0
  35. data/spec/lib/secure_headers_spec.rb +204 -278
  36. data/spec/spec_helper.rb +32 -28
  37. data/upgrading-to-3-0.md +37 -0
  38. metadata +14 -94
  39. data/fixtures/rails_3_2_12/.rspec +0 -1
  40. data/fixtures/rails_3_2_12/Gemfile +0 -6
  41. data/fixtures/rails_3_2_12/README.rdoc +0 -261
  42. data/fixtures/rails_3_2_12/Rakefile +0 -7
  43. data/fixtures/rails_3_2_12/app/controllers/application_controller.rb +0 -4
  44. data/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb +0 -5
  45. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -5
  46. data/fixtures/rails_3_2_12/app/models/.gitkeep +0 -0
  47. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -11
  48. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +0 -2
  49. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +0 -1
  50. data/fixtures/rails_3_2_12/config/application.rb +0 -14
  51. data/fixtures/rails_3_2_12/config/boot.rb +0 -6
  52. data/fixtures/rails_3_2_12/config/environment.rb +0 -5
  53. data/fixtures/rails_3_2_12/config/environments/test.rb +0 -37
  54. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +0 -17
  55. data/fixtures/rails_3_2_12/config/routes.rb +0 -4
  56. data/fixtures/rails_3_2_12/config/script_hashes.yml +0 -5
  57. data/fixtures/rails_3_2_12/config.ru +0 -7
  58. data/fixtures/rails_3_2_12/lib/assets/.gitkeep +0 -0
  59. data/fixtures/rails_3_2_12/lib/tasks/.gitkeep +0 -0
  60. data/fixtures/rails_3_2_12/log/.gitkeep +0 -0
  61. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +0 -83
  62. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +0 -54
  63. data/fixtures/rails_3_2_12/spec/spec_helper.rb +0 -15
  64. data/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep +0 -0
  65. data/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep +0 -0
  66. data/fixtures/rails_3_2_12/vendor/plugins/.gitkeep +0 -0
  67. data/fixtures/rails_3_2_12_no_init/.rspec +0 -1
  68. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -5
  69. data/fixtures/rails_3_2_12_no_init/README.rdoc +0 -261
  70. data/fixtures/rails_3_2_12_no_init/Rakefile +0 -7
  71. data/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb +0 -4
  72. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -20
  73. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +0 -5
  74. data/fixtures/rails_3_2_12_no_init/app/models/.gitkeep +0 -0
  75. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -12
  76. data/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb +0 -1
  77. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -0
  78. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -17
  79. data/fixtures/rails_3_2_12_no_init/config/boot.rb +0 -6
  80. data/fixtures/rails_3_2_12_no_init/config/environment.rb +0 -5
  81. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +0 -37
  82. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -4
  83. data/fixtures/rails_3_2_12_no_init/config.ru +0 -4
  84. data/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep +0 -0
  85. data/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep +0 -0
  86. data/fixtures/rails_3_2_12_no_init/log/.gitkeep +0 -0
  87. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
  88. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +0 -54
  89. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +0 -5
  90. data/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep +0 -0
  91. data/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
  92. data/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep +0 -0
  93. data/fixtures/rails_4_1_8/Gemfile +0 -5
  94. data/fixtures/rails_4_1_8/README.rdoc +0 -28
  95. data/fixtures/rails_4_1_8/Rakefile +0 -6
  96. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
  97. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  98. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
  99. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
  100. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  101. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  102. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
  103. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
  104. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
  105. data/fixtures/rails_4_1_8/config/application.rb +0 -15
  106. data/fixtures/rails_4_1_8/config/boot.rb +0 -4
  107. data/fixtures/rails_4_1_8/config/environment.rb +0 -5
  108. data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
  109. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -17
  110. data/fixtures/rails_4_1_8/config/routes.rb +0 -4
  111. data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
  112. data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
  113. data/fixtures/rails_4_1_8/config.ru +0 -4
  114. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  115. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  116. data/fixtures/rails_4_1_8/log/.keep +0 -0
  117. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
  118. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
  119. data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
  120. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  121. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  122. data/lib/secure_headers/hash_helper.rb +0 -7
  123. data/lib/secure_headers/header.rb +0 -5
  124. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
  125. data/lib/secure_headers/version.rb +0 -3
  126. data/lib/tasks/tasks.rake +0 -48
  127. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -47
@@ -2,328 +2,241 @@ require 'spec_helper'
2
2
 
3
3
  module SecureHeaders
4
4
  describe ContentSecurityPolicy do
5
- let(:default_opts) do
5
+ let (:default_opts) do
6
6
  {
7
- :disable_fill_missing => true,
8
- :default_src => 'https:',
9
- :report_uri => '/csp_report',
10
- :script_src => 'inline eval https: data:',
11
- :style_src => "inline https: about:"
7
+ default_src: %w(https:),
8
+ img_src: %w(https: data:),
9
+ script_src: %w('unsafe-inline' 'unsafe-eval' https: data:),
10
+ style_src: %w('unsafe-inline' https: about:),
11
+ report_uri: %w(/csp_report)
12
12
  }
13
13
  end
14
14
 
15
- IE = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
16
- FIREFOX = "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8"
17
- FIREFOX_23 = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0"
18
- 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"
19
- CHROME_25 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/537.22 (KHTML like Gecko) Chrome/25.0.1364.99 Safari/537.22"
20
- SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A"
21
- OPERA = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16"
22
-
23
- def request_for user_agent, request_uri=nil, options={:ssl => false}
24
- double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
25
- end
26
-
27
- before(:each) do
28
- @options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
29
- end
30
-
31
15
  describe "#name" do
32
- context "when supplying options to override request" do
33
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
34
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
35
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
36
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
37
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
38
- end
39
-
40
16
  context "when in report-only mode" do
41
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")}
42
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
43
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
44
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
45
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
17
+ specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
46
18
  end
47
19
 
48
20
  context "when in enforce mode" do
49
- let(:opts) { default_opts.merge(:enforce => true)}
50
-
51
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
52
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
53
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
54
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
55
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
21
+ specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
56
22
  end
57
23
  end
58
24
 
59
- it "exports a policy to JSON" do
60
- policy = ContentSecurityPolicy.new(default_opts)
61
- expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
62
- expect(policy.to_json).to eq(expected)
63
- end
64
-
65
- it "imports JSON to build a policy" do
66
- json1 = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"]})
67
- json2 = %({"style-src":["'unsafe-inline'"],"img-src":["https:","data:"]})
68
- json3 = %({"style-src":["https:","about:"]})
69
- config = ContentSecurityPolicy.from_json(json1, json2, json3)
70
- policy = ContentSecurityPolicy.new(config.merge(:disable_fill_missing => true))
71
-
72
- expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
73
- expect(policy.to_json).to eq(expected)
74
- end
25
+ describe "#validate_config!" do
26
+ it "accepts all keys" do
27
+ # (pulled from README)
28
+ config = {
29
+ # "meta" values. these will shaped the header, but the values are not included in the header.
30
+ report_only: true, # default: false
31
+ preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
32
+
33
+ # directive values: these values will directly translate into source directives
34
+ default_src: %w(https: 'self'),
35
+ frame_src: %w('self' *.twimg.com itunes.apple.com),
36
+ connect_src: %w(wws:),
37
+ font_src: %w('self' data:),
38
+ img_src: %w(mycdn.com data:),
39
+ media_src: %w(utoob.com),
40
+ object_src: %w('self'),
41
+ script_src: %w('self'),
42
+ style_src: %w('unsafe-inline'),
43
+ base_uri: %w('self'),
44
+ child_src: %w('self'),
45
+ form_action: %w('self' github.com),
46
+ frame_ancestors: %w('none'),
47
+ plugin_types: %w(application/x-shockwave-flash),
48
+ block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
49
+ report_uri: %w(https://example.com/uri-directive)
50
+ }
75
51
 
76
- context "when using hash sources" do
77
- it "adds hashes and unsafe-inline to the script-src" do
78
- policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
79
- expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
52
+ CSP.validate_config!(config)
80
53
  end
81
- end
82
54
 
83
- describe "#normalize_csp_options" do
84
- before(:each) do
85
- default_opts.delete(:disable_fill_missing)
86
- default_opts[:script_src] << ' self none'
87
- @opts = default_opts
55
+ it "requires a :default_src value" do
56
+ expect do
57
+ CSP.validate_config!(script_src: %('self'))
58
+ end.to raise_error(ContentSecurityPolicyConfigError)
88
59
  end
89
60
 
90
- context "Content-Security-Policy" do
91
- it "converts the script values to their equivilents" do
92
- csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
93
- expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
94
- end
95
-
96
- it "adds a @enforce and @app_name variables to the report uri" do
97
- opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => proc { 'twitter' })
98
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
99
- expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
100
- end
101
-
102
- it "does not add an empty @app_name variable to the report uri" do
103
- opts = @opts.merge(:tag_report_uri => true, :enforce => true)
104
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
105
- expect(csp.value).to include("/csp_report?enforce=true")
106
- end
61
+ it "requires :report_only to be a truthy value" do
62
+ expect do
63
+ CSP.validate_config!(default_opts.merge(report_only: "steve"))
64
+ end.to raise_error(ContentSecurityPolicyConfigError)
65
+ end
107
66
 
108
- it "accepts procs for report-uris" do
109
- opts = {
110
- :default_src => 'self',
111
- :report_uri => proc { "http://lambda/result" }
112
- }
67
+ it "requires :preserve_schemes to be a truthy value" do
68
+ expect do
69
+ CSP.validate_config!(default_opts.merge(preserve_schemes: "steve"))
70
+ end.to raise_error(ContentSecurityPolicyConfigError)
71
+ end
113
72
 
114
- csp = ContentSecurityPolicy.new(opts)
115
- expect(csp.value).to match("report-uri http://lambda/result")
116
- end
73
+ it "requires :block_all_mixed_content to be a boolean value" do
74
+ expect do
75
+ CSP.validate_config!(default_opts.merge(block_all_mixed_content: "steve"))
76
+ end.to raise_error(ContentSecurityPolicyConfigError)
77
+ end
117
78
 
118
- it "accepts procs for other fields" do
119
- opts = {
120
- :default_src => proc { "http://lambda/result" },
121
- :enforce => proc { true },
122
- :disable_fill_missing => proc { true }
123
- }
79
+ it "requires all source lists to be an array of strings" do
80
+ expect do
81
+ CSP.validate_config!(default_src: "steve")
82
+ end.to raise_error(ContentSecurityPolicyConfigError)
83
+ end
124
84
 
125
- csp = ContentSecurityPolicy.new(opts)
126
- expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
127
- expect(csp.name).to match("Content-Security-Policy")
128
- end
85
+ it "allows nil values" do
86
+ expect do
87
+ CSP.validate_config!(default_src: %w('self'), script_src: ["https:", nil])
88
+ end.to_not raise_error
89
+ end
129
90
 
130
- it "passes a reference to the controller to the proc" do
131
- controller = double
132
- user = double(:beta_testing? => true)
91
+ it "rejects unknown directives / config" do
92
+ expect do
93
+ CSP.validate_config!(default_src: %w('self'), default_src_totally_mispelled: "steve")
94
+ end.to raise_error(ContentSecurityPolicyConfigError)
95
+ end
133
96
 
134
- allow(controller).to receive(:current_user).and_return(user)
135
- opts = {
136
- :disable_fill_missing => true,
137
- :default_src => "self",
138
- :enforce => lambda { |c| c.current_user.beta_testing? }
139
- }
140
- csp = ContentSecurityPolicy.new(opts, :controller => controller)
141
- expect(csp.name).to match("Content-Security-Policy")
142
- end
97
+ # this is mostly to ensure people don't use the antiquated shorthands common in other configs
98
+ it "performs light validation on source lists" do
99
+ expect do
100
+ CSP.validate_config!(default_src: %w(self none inline eval))
101
+ end.to raise_error(ContentSecurityPolicyConfigError)
143
102
  end
144
103
  end
145
104
 
146
- describe "#value" do
147
- it "raises an exception when default-src is missing" do
148
- csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
149
- expect {
150
- csp.value
151
- }.to raise_error(RuntimeError)
105
+ describe "#combine_policies" do
106
+ it "combines the default-src value with the override if the directive was unconfigured" do
107
+ combined_config = CSP.combine_policies(Configuration.default.csp, script_src: %w(anothercdn.com))
108
+ csp = ContentSecurityPolicy.new(combined_config)
109
+ expect(csp.name).to eq(CSP::HEADER_NAME)
110
+ expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
152
111
  end
153
112
 
154
- context "CSP level 2 directives" do
155
- let(:config) { {:default_src => 'self'} }
156
- ::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
157
- it "supports all level 2 directives" do
158
- directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
159
- config.merge!({ non_default_source => "value" })
160
- csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
161
- expect(csp.value).to match(/#{directive_name} value;/)
162
- end
113
+ it "overrides the report_only flag" do
114
+ Configuration.default do |config|
115
+ config.csp = {
116
+ default_src: %w('self'),
117
+ report_only: false
118
+ }
163
119
  end
120
+ combined_config = CSP.combine_policies(Configuration.get.csp, report_only: true)
121
+ csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
122
+ expect(csp.name).to eq(CSP::REPORT_ONLY)
164
123
  end
165
124
 
166
- context "auto-whitelists data: uris for img-src" do
167
- it "sets the value if no img-src specified" do
168
- csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
169
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
125
+ it "overrides the :block_all_mixed_content flag" do
126
+ Configuration.default do |config|
127
+ config.csp = {
128
+ default_src: %w(https:),
129
+ block_all_mixed_content: false
130
+ }
170
131
  end
132
+ combined_config = CSP.combine_policies(Configuration.get.csp, block_all_mixed_content: true)
133
+ csp = ContentSecurityPolicy.new(combined_config)
134
+ expect(csp.value).to eq("default-src https:; block-all-mixed-content")
135
+ end
171
136
 
172
- it "appends the value if img-src is specified" do
173
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
174
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
137
+ it "raises an error if appending to a OPT_OUT policy" do
138
+ Configuration.default do |config|
139
+ config.csp = OPT_OUT
175
140
  end
141
+ expect do
142
+ CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
143
+ end.to raise_error(ContentSecurityPolicyConfigError)
144
+ end
145
+ end
176
146
 
177
- it "doesn't add a duplicate data uri if img-src specifies it already" do
178
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self data:', :disable_fill_missing => true}, :request => request_for(CHROME))
179
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
180
- end
147
+ describe "#idempotent_additions?" do
148
+ specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
149
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
150
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
151
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
152
+
153
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
154
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
155
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
156
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
157
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
158
+ specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
159
+ end
181
160
 
182
- it "allows the user to disable img-src data: uris auto-whitelisting" do
183
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_img_src_data_uri => true, :disable_fill_missing => true}, :request => request_for(CHROME))
184
- expect(csp.value).to eq("default-src 'self'; img-src 'self';")
185
- end
161
+ describe "#value" do
162
+ it "discards 'none' values if any other source expressions are present" do
163
+ csp = ContentSecurityPolicy.new(default_opts.merge(frame_src: %w('self' 'none')))
164
+ expect(csp.value).not_to include("'none'")
186
165
  end
187
166
 
188
- it "fills in directives without values with default-src value" do
189
- options = default_opts.merge(:disable_fill_missing => false)
190
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
191
- 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: about:; report-uri /csp_report;"
192
- expect(csp.value).to eq(value)
167
+ it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do
168
+ csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:))
169
+ expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:")
193
170
  end
194
171
 
195
- it "sends the standard csp header if an unknown browser is supplied" do
196
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE))
197
- expect(csp.value).to match "default-src"
172
+ it "minifies source expressions based on overlapping wildcards" do
173
+ config = {
174
+ default_src: %w(a.example.org b.example.org *.example.org https://*.example.org)
175
+ }
176
+ csp = ContentSecurityPolicy.new(config)
177
+ expect(csp.value).to eq("default-src *.example.org")
198
178
  end
199
179
 
200
- context "Firefox" do
201
- it "builds a csp header for firefox" do
202
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
203
- expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
204
- end
180
+ it "removes http/s schemes from hosts" do
181
+ csp = ContentSecurityPolicy.new(default_src: %w(https://example.org))
182
+ expect(csp.value).to eq("default-src example.org")
205
183
  end
206
184
 
207
- context "Chrome" do
208
- it "builds a csp header for chrome" do
209
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
210
- expect(csp.value).to eq("default-src https:; img-src https: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
211
- end
185
+ it "does not remove schemes from report-uri values" do
186
+ csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org))
187
+ expect(csp.value).to eq("default-src https:; report-uri https://example.org")
212
188
  end
213
189
 
214
- context "when using a nonce" do
215
- it "adds a nonce and unsafe-inline to the script-src value when using chrome" do
216
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
217
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
218
- end
219
-
220
- it "adds a nonce and unsafe-inline to the script-src value when using firefox" do
221
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(FIREFOX))
222
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
223
- end
224
-
225
- it "adds a nonce and unsafe-inline to the script-src value when using opera" do
226
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(OPERA))
227
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
228
- end
229
-
230
- it "does not add a nonce and unsafe-inline to the script-src value when using Safari" do
231
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(SAFARI))
232
- expect(header.value).to include("script-src 'self' 'unsafe-inline'")
233
- expect(header.value).not_to include("nonce")
234
- end
235
-
236
- it "does not add a nonce and unsafe-inline to the script-src value when using IE" do
237
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(IE))
238
- expect(header.value).to include("script-src 'self' 'unsafe-inline'")
239
- expect(header.value).not_to include("nonce")
240
- end
241
-
242
- it "adds a nonce and unsafe-inline to the style-src value" do
243
- header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
244
- expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
245
- end
190
+ it "does not remove schemes when :preserve_schemes is true" do
191
+ csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true)
192
+ expect(csp.value).to eq("default-src https://example.org")
193
+ end
246
194
 
247
- it "adds an identical nonce to the style and script-src directives" do
248
- header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
249
- nonce = header.nonce
250
- value = header.value
251
- expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
252
- expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
253
- end
195
+ it "removes nil from source lists" do
196
+ csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil])
197
+ expect(csp.value).to eq("default-src example.org")
198
+ end
254
199
 
255
- it "does not add 'unsafe-inline' twice" do
256
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
257
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
258
- end
200
+ it "does not add a directive if the value is an empty array (or all nil)" do
201
+ csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil])
202
+ expect(csp.value).to eq("default-src example.org")
259
203
  end
260
204
 
261
- context "when supplying additional http directive values" do
262
- let(:options) {
263
- default_opts.merge({
264
- :http_additions => {
265
- :frame_src => "http:",
266
- :img_src => "http:"
267
- }
268
- })
269
- }
205
+ it "does not add a directive if the value is nil" do
206
+ csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil)
207
+ expect(csp.value).to eq("default-src example.org")
208
+ end
270
209
 
271
- it "adds directive values for headers on http" do
272
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
273
- expect(csp.value).to eq("default-src https:; frame-src http:; img-src http: data:; script-src 'unsafe-inline' 'unsafe-eval' https: data:; style-src 'unsafe-inline' https: about:; report-uri /csp_report;")
274
- end
210
+ it "deduplicates any source expressions" do
211
+ csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
212
+ expect(csp.value).to eq("default-src example.org")
213
+ end
275
214
 
276
- it "does not add the directive values if requesting https" do
277
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME, '/', :ssl => true))
278
- expect(csp.value).not_to match(/http:/)
215
+ context "browser sniffing" do
216
+ let (:complex_opts) do
217
+ ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = %w('self') }
218
+ .merge(block_all_mixed_content: true, reflected_xss: "block")
219
+ .merge(script_src: %w('self'), script_nonce: 123456)
279
220
  end
280
221
 
281
- it "does not add the directive values if requesting https" do
282
- csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
283
- expect(csp.value).not_to match(/http:/)
222
+ it "does not filter any directives for Chrome" do
223
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
224
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
284
225
  end
285
- end
286
226
 
287
- describe "class methods" do
288
- let(:ua) { CHROME }
289
- let(:env) do
290
- double.tap do |env|
291
- allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
292
- end
293
- end
294
- let(:request) do
295
- double(
296
- :ssl? => true,
297
- :url => 'https://example.com',
298
- :env => env
299
- )
227
+ it "does not filter any directives for Opera" do
228
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
229
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; block-all-mixed-content; child-src 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; plugin-types 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
300
230
  end
301
231
 
302
- describe ".add_to_env" do
303
- let(:controller) { double }
304
- let(:config) { {:default_src => 'self'} }
305
- let(:options) { {:controller => controller} }
306
-
307
- it "adds metadata to env" do
308
- metadata = {
309
- :config => config,
310
- :options => options
311
- }
312
- expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
313
- expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
314
- ContentSecurityPolicy.add_to_env(request, controller, config)
315
- end
232
+ it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
233
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
234
+ expect(policy.value).to eq("default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'nonce-123456'; style-src 'self'; report-uri 'self'")
316
235
  end
317
236
 
318
- describe ".options_from_request" do
319
- it "extracts options from request" do
320
- options = ContentSecurityPolicy.options_from_request(request)
321
- expect(options).to eql({
322
- :ua => ua,
323
- :ssl => true,
324
- :request_uri => 'https://example.com'
325
- })
326
- end
237
+ it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
238
+ policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
239
+ expect(policy.value).to eq("default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; media-src 'self'; object-src 'self'; sandbox 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; report-uri 'self'")
327
240
  end
328
241
  end
329
242
  end
@@ -2,35 +2,35 @@ require 'spec_helper'
2
2
 
3
3
  module SecureHeaders
4
4
  describe PublicKeyPins do
5
- specify{ expect(PublicKeyPins.new(:max_age => 1234).name).to eq("Public-Key-Pins-Report-Only") }
6
- specify{ expect(PublicKeyPins.new(:max_age => 1234, :enforce => true).name).to eq("Public-Key-Pins") }
5
+ specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") }
6
+ specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") }
7
7
 
8
- specify { expect(PublicKeyPins.new({:max_age => 1234}).value).to eq("max-age=1234")}
9
- specify { expect(PublicKeyPins.new(:max_age => 1234).value).to eq("max-age=1234")}
10
- specify {
11
- config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin1'}, {:sha256 => 'base64encodedpin2'}]}
8
+ specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
9
+ specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") }
10
+ specify do
11
+ config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] }
12
12
  header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\""
13
13
  expect(PublicKeyPins.new(config).value).to eq(header_value)
14
- }
14
+ end
15
15
 
16
16
  context "with an invalid configuration" do
17
17
  it "raises an exception when max-age is not provided" do
18
- expect {
19
- PublicKeyPins.new(:foo => 'bar')
20
- }.to raise_error(PublicKeyPinsBuildError)
18
+ expect do
19
+ PublicKeyPins.validate_config!(foo: 'bar')
20
+ end.to raise_error(PublicKeyPinsConfigError)
21
21
  end
22
22
 
23
23
  it "raises an exception with an invalid max-age" do
24
- expect {
25
- PublicKeyPins.new(:max_age => 'abc123')
26
- }.to raise_error(PublicKeyPinsBuildError)
24
+ expect do
25
+ PublicKeyPins.validate_config!(max_age: 'abc123')
26
+ end.to raise_error(PublicKeyPinsConfigError)
27
27
  end
28
28
 
29
29
  it 'raises an exception with less than 2 pins' do
30
- expect {
31
- config = {:max_age => 1234, :pins => [{:sha256 => 'base64encodedpin'}]}
32
- PublicKeyPins.new(config)
33
- }.to raise_error(PublicKeyPinsBuildError)
30
+ expect do
31
+ config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] }
32
+ PublicKeyPins.validate_config!(config)
33
+ end.to raise_error(PublicKeyPinsConfigError)
34
34
  end
35
35
  end
36
36
  end