secure_headers 2.5.3 → 3.0.0.pre

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 (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