secure_headers 2.5.3 → 3.0.0.pre

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.

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