secure_headers 2.2.0 → 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 -1
  5. data/CHANGELOG.md +205 -0
  6. data/Gemfile +9 -10
  7. data/Guardfile +12 -0
  8. data/README.md +175 -276
  9. data/Rakefile +2 -36
  10. data/lib/secure_headers/configuration.rb +189 -0
  11. data/lib/secure_headers/headers/content_security_policy.rb +367 -188
  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 +244 -145
  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 +176 -213
  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 -25
  37. data/upgrading-to-3-0.md +37 -0
  38. metadata +28 -95
  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 -6
  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 -50
  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,278 +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
-
15
+ describe "#name" do
16
+ context "when in report-only mode" do
17
+ specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicy::HEADER_NAME + "-Report-Only") }
18
+ end
21
19
 
22
- def request_for user_agent, request_uri=nil, options={:ssl => false}
23
- double(:ssl? => options[:ssl], :env => {'HTTP_USER_AGENT' => user_agent}, :url => (request_uri || 'http://areallylongdomainexample.com') )
20
+ context "when in enforce mode" do
21
+ specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicy::HEADER_NAME) }
22
+ end
24
23
  end
25
24
 
26
- before(:each) do
27
- @options_with_forwarding = default_opts.merge(:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com')
28
- 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
+ }
29
51
 
30
- describe "#name" do
31
- context "when supplying options to override request" do
32
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(HEADER_NAME + "-Report-Only")}
33
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(HEADER_NAME + "-Report-Only")}
34
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(HEADER_NAME + "-Report-Only")}
35
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(HEADER_NAME + "-Report-Only")}
36
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(HEADER_NAME + "-Report-Only")}
52
+ CSP.validate_config!(config)
37
53
  end
38
54
 
39
- context "when in report-only mode" do
40
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(HEADER_NAME + "-Report-Only")}
41
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME + "-Report-Only")}
42
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME + "-Report-Only")}
43
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME + "-Report-Only")}
44
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME + "-Report-Only")}
55
+ it "requires a :default_src value" do
56
+ expect do
57
+ CSP.validate_config!(script_src: %('self'))
58
+ end.to raise_error(ContentSecurityPolicyConfigError)
45
59
  end
46
60
 
47
- context "when in enforce mode" do
48
- let(:opts) { default_opts.merge(:enforce => true)}
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
49
66
 
50
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(HEADER_NAME)}
51
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(HEADER_NAME)}
52
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(HEADER_NAME)}
53
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(HEADER_NAME)}
54
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(HEADER_NAME)}
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)
55
71
  end
56
- end
57
72
 
58
- context "when using hash sources" do
59
- it "adds hashes and unsafe-inline to the script-src" do
60
- policy = ContentSecurityPolicy.new(default_opts.merge(:script_hashes => ['sha256-abc123']))
61
- expect(policy.value).to match /script-src[^;]*'sha256-abc123'/
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)
62
77
  end
63
- end
64
78
 
65
- describe "#normalize_csp_options" do
66
- before(:each) do
67
- default_opts.delete(:disable_fill_missing)
68
- default_opts[:script_src] << ' self none'
69
- @opts = default_opts
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)
70
83
  end
71
84
 
72
- context "Content-Security-Policy" do
73
- it "converts the script values to their equivilents" do
74
- csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
75
- expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
76
- 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
77
90
 
78
- it "adds a @enforce and @app_name variables to the report uri" do
79
- opts = @opts.merge(:tag_report_uri => true, :enforce => true, :app_name => proc { 'twitter' })
80
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
81
- expect(csp.value).to include("/csp_report?enforce=true&app_name=twitter")
82
- end
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
83
96
 
84
- it "does not add an empty @app_name variable to the report uri" do
85
- opts = @opts.merge(:tag_report_uri => true, :enforce => true)
86
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
87
- expect(csp.value).to include("/csp_report?enforce=true")
88
- 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)
102
+ end
103
+ end
89
104
 
90
- it "accepts procs for report-uris" do
91
- opts = {
92
- :default_src => 'self',
93
- :report_uri => proc { "http://lambda/result" }
94
- }
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")
111
+ end
95
112
 
96
- csp = ContentSecurityPolicy.new(opts)
97
- expect(csp.value).to match("report-uri http://lambda/result")
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
+ }
98
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)
123
+ end
99
124
 
100
- it "accepts procs for other fields" do
101
- opts = {
102
- :default_src => proc { "http://lambda/result" },
103
- :enforce => proc { true },
104
- :disable_fill_missing => proc { true }
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
105
130
  }
106
-
107
- csp = ContentSecurityPolicy.new(opts)
108
- expect(csp.value).to eq("default-src http://lambda/result; img-src http://lambda/result data:;")
109
- expect(csp.name).to match("Content-Security-Policy")
110
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
111
136
 
112
- it "passes a reference to the controller to the proc" do
113
- controller = double
114
- user = double(:beta_testing? => true)
115
-
116
- allow(controller).to receive(:current_user).and_return(user)
117
- opts = {
118
- :disable_fill_missing => true,
119
- :default_src => "self",
120
- :enforce => lambda { |c| c.current_user.beta_testing? }
121
- }
122
- csp = ContentSecurityPolicy.new(opts, :controller => controller)
123
- expect(csp.name).to match("Content-Security-Policy")
137
+ it "raises an error if appending to a OPT_OUT policy" do
138
+ Configuration.default do |config|
139
+ config.csp = OPT_OUT
124
140
  end
141
+ expect do
142
+ CSP.combine_policies(Configuration.get.csp, script_src: %w(anothercdn.com))
143
+ end.to raise_error(ContentSecurityPolicyConfigError)
125
144
  end
126
145
  end
127
146
 
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
160
+
128
161
  describe "#value" do
129
- it "raises an exception when default-src is missing" do
130
- csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
131
- expect {
132
- csp.value
133
- }.to raise_error(RuntimeError)
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'")
134
165
  end
135
166
 
136
- context "CSP level 2 directives" do
137
- let(:config) { {:default_src => 'self'} }
138
- ::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
139
- it "supports all level 2 directives" do
140
- directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
141
- config.merge!({ non_default_source => "value" })
142
- csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
143
- expect(csp.value).to match(/#{directive_name} value;/)
144
- end
145
- end
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:")
146
170
  end
147
171
 
148
- context "auto-whitelists data: uris for img-src" do
149
- it "sets the value if no img-src specified" do
150
- csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
151
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
152
- end
153
-
154
- it "appends the value if img-src is specified" do
155
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
156
- expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
157
- end
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")
158
178
  end
159
179
 
160
- it "fills in directives without values with default-src value" do
161
- options = default_opts.merge(:disable_fill_missing => false)
162
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
163
- 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;"
164
- expect(csp.value).to eq(value)
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")
165
183
  end
166
184
 
167
- it "sends the standard csp header if an unknown browser is supplied" do
168
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(IE))
169
- expect(csp.value).to match "default-src"
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")
170
188
  end
171
189
 
172
- context "Firefox" do
173
- it "builds a csp header for firefox" do
174
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
175
- 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;")
176
- 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")
177
193
  end
178
194
 
179
- context "Chrome" do
180
- it "builds a csp header for chrome" do
181
- csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
182
- 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;")
183
- 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")
184
198
  end
185
199
 
186
- context "when using a nonce" do
187
- it "adds a nonce and unsafe-inline to the script-src value" do
188
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
189
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
190
- end
191
-
192
- it "adds a nonce and unsafe-inline to the style-src value" do
193
- header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
194
- expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
195
- end
196
-
197
- it "adds an identical nonce to the style and script-src directives" do
198
- header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
199
- nonce = header.nonce
200
- value = header.value
201
- expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
202
- expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
203
- end
204
-
205
- it "does not add 'unsafe-inline' twice" do
206
- header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
207
- expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
208
- 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")
209
203
  end
210
204
 
211
- context "when supplying additional http directive values" do
212
- let(:options) {
213
- default_opts.merge({
214
- :http_additions => {
215
- :frame_src => "http:",
216
- :img_src => "http:"
217
- }
218
- })
219
- }
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
220
209
 
221
- it "adds directive values for headers on http" do
222
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
223
- 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;")
224
- 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
225
214
 
226
- it "does not add the directive values if requesting https" do
227
- csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME, '/', :ssl => true))
228
- 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)
229
220
  end
230
221
 
231
- it "does not add the directive values if requesting https" do
232
- csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
233
- 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'")
234
225
  end
235
- end
236
226
 
237
- describe "class methods" do
238
- let(:ua) { CHROME }
239
- let(:env) do
240
- double.tap do |env|
241
- allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
242
- end
243
- end
244
- let(:request) do
245
- double(
246
- :ssl? => true,
247
- :url => 'https://example.com',
248
- :env => env
249
- )
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'")
250
230
  end
251
231
 
252
- describe ".add_to_env" do
253
- let(:controller) { double }
254
- let(:config) { {:default_src => 'self'} }
255
- let(:options) { {:controller => controller} }
256
-
257
- it "adds metadata to env" do
258
- metadata = {
259
- :config => config,
260
- :options => options
261
- }
262
- expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
263
- expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
264
- ContentSecurityPolicy.add_to_env(request, controller, config)
265
- 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'")
266
235
  end
267
236
 
268
- describe ".options_from_request" do
269
- it "extracts options from request" do
270
- options = ContentSecurityPolicy.options_from_request(request)
271
- expect(options).to eql({
272
- :ua => ua,
273
- :ssl => true,
274
- :request_uri => 'https://example.com'
275
- })
276
- 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'")
277
240
  end
278
241
  end
279
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
@@ -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