secure_headers 1.2.0 → 2.0.2

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 (121) hide show
  1. data/.gitignore +4 -8
  2. data/.travis.yml +7 -4
  3. data/Gemfile +6 -9
  4. data/README.md +185 -105
  5. data/Rakefile +11 -116
  6. data/fixtures/rails_3_2_12/Gemfile +0 -8
  7. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
  8. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
  9. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  10. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
  11. data/fixtures/rails_3_2_12/config/application.rb +0 -54
  12. data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
  13. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +3 -1
  14. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  15. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  16. data/fixtures/rails_3_2_12/config.ru +3 -0
  17. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +59 -16
  18. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +11 -1
  19. data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -4
  20. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -9
  21. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  22. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  23. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  24. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  25. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
  26. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
  27. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  28. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +11 -1
  29. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +10 -0
  30. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -17
  31. data/fixtures/rails_4_1_8/Gemfile +5 -0
  32. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  33. data/fixtures/rails_4_1_8/Rakefile +6 -0
  34. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  35. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  36. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  37. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  38. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  39. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  40. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  41. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  42. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  43. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  44. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  45. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  46. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  47. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  48. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  49. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  50. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  51. data/fixtures/rails_4_1_8/config.ru +4 -0
  52. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  53. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  54. data/fixtures/rails_4_1_8/log/.keep +0 -0
  55. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  56. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  57. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  58. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  59. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  60. data/lib/secure_headers/hash_helper.rb +7 -0
  61. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  62. data/lib/secure_headers/headers/content_security_policy.rb +160 -124
  63. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  64. data/lib/secure_headers/headers/x_download_options.rb +39 -0
  65. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  66. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  67. data/lib/secure_headers/padrino.rb +14 -0
  68. data/lib/secure_headers/railtie.rb +19 -24
  69. data/lib/secure_headers/version.rb +1 -1
  70. data/lib/secure_headers/view_helper.rb +68 -0
  71. data/lib/secure_headers.rb +65 -16
  72. data/lib/tasks/tasks.rake +48 -0
  73. data/secure_headers.gemspec +4 -4
  74. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  75. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +119 -204
  76. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +1 -0
  77. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
  78. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  79. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +2 -1
  80. data/spec/lib/secure_headers_spec.rb +61 -66
  81. data/spec/spec_helper.rb +29 -17
  82. metadata +55 -47
  83. checksums.yaml +0 -15
  84. data/Guardfile +0 -6
  85. data/HISTORY.md +0 -131
  86. data/app/controllers/content_security_policy_controller.rb +0 -75
  87. data/config/curl-ca-bundle.crt +0 -5420
  88. data/config/routes.rb +0 -3
  89. data/fixtures/rails_3_2_12/Guardfile +0 -14
  90. data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
  91. data/fixtures/rails_3_2_12/config/database.yml +0 -25
  92. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  93. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  94. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  95. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  96. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  97. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  98. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  99. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  100. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  101. data/fixtures/rails_3_2_12/db/schema.rb +0 -16
  102. data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
  103. data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
  104. data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
  105. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  106. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  107. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  108. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  109. data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
  110. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  111. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  112. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  113. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  114. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  115. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  116. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  117. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  118. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  119. data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
  120. data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
  121. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -4,12 +4,11 @@ module SecureHeaders
4
4
  describe ContentSecurityPolicy do
5
5
  let(:default_opts) do
6
6
  {
7
- :disable_chrome_extension => true,
8
7
  :disable_fill_missing => true,
9
- :default_src => 'https://*',
8
+ :default_src => 'https:',
10
9
  :report_uri => '/csp_report',
11
- :script_src => 'inline eval https://* data:',
12
- :style_src => "inline https://* about:"
10
+ :script_src => 'inline eval https: data:',
11
+ :style_src => "inline https: about:"
13
12
  }
14
13
  end
15
14
 
@@ -30,44 +29,41 @@ module SecureHeaders
30
29
 
31
30
  describe "#name" do
32
31
  context "when supplying options to override request" do
33
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => IE).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
34
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
35
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => FIREFOX_23).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
36
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
37
- specify { expect(ContentSecurityPolicy.new(default_opts, :ua => CHROME_25).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
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")}
38
37
  end
39
38
 
40
39
  context "when in report-only mode" do
41
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
42
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
43
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
44
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
45
- specify { expect(ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
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")}
46
45
  end
47
46
 
48
47
  context "when in enforce mode" do
49
48
  let(:opts) { default_opts.merge(:enforce => true)}
50
49
 
51
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(IE)).name).to eq(STANDARD_HEADER_NAME)}
52
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX)).name).to eq(STANDARD_HEADER_NAME)}
53
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX_23)).name).to eq(STANDARD_HEADER_NAME)}
54
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME)).name).to eq(STANDARD_HEADER_NAME)}
55
- specify { expect(ContentSecurityPolicy.new(opts, :request => request_for(CHROME_25)).name).to eq(STANDARD_HEADER_NAME)}
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)}
56
55
  end
56
+ end
57
57
 
58
- context "when in experimental mode" do
59
- let(:opts) { default_opts.merge(:enforce => true).merge(:experimental => {})}
60
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(IE)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
61
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
62
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(FIREFOX_23)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
63
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
64
- specify { expect(ContentSecurityPolicy.new(opts, {:experimental => true, :request => request_for(CHROME_25)}).name).to eq(STANDARD_HEADER_NAME + "-Report-Only")}
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'/
65
62
  end
66
63
  end
67
64
 
68
65
  describe "#normalize_csp_options" do
69
66
  before(:each) do
70
- default_opts.delete(:disable_chrome_extension)
71
67
  default_opts.delete(:disable_fill_missing)
72
68
  default_opts[:script_src] << ' self none'
73
69
  @opts = default_opts
@@ -76,7 +72,19 @@ module SecureHeaders
76
72
  context "Content-Security-Policy" do
77
73
  it "converts the script values to their equivilents" do
78
74
  csp = ContentSecurityPolicy.new(@opts, :request => request_for(CHROME))
79
- expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https://* data: 'self' 'none'")
75
+ expect(csp.value).to include("script-src 'unsafe-inline' 'unsafe-eval' https: data: 'self' 'none'")
76
+ end
77
+
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 => lambda { '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
83
+
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")
80
88
  end
81
89
 
82
90
  it "accepts procs for report-uris" do
@@ -86,124 +94,19 @@ module SecureHeaders
86
94
  }
87
95
 
88
96
  csp = ContentSecurityPolicy.new(opts)
89
- expect(csp.report_uri).to eq("http://lambda/result")
97
+ expect(csp.value).to match("report-uri http://lambda/result")
90
98
  end
91
99
 
92
100
  it "accepts procs for other fields" do
93
101
  opts = {
94
- :default_src => lambda { "http://lambda/result" }
95
- }
96
-
97
- csp = ContentSecurityPolicy.new(opts).value
98
- expect(csp).to match("default-src http://lambda/result")
99
- end
100
- end
101
- end
102
-
103
- describe "#same_origin?" do
104
- let(:origin) {"https://example.com:123"}
105
-
106
- it "matches when host, scheme, and port match" do
107
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com"))
108
- expect(csp.send(:same_origin?)).to be true
109
-
110
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://example.com:443"))
111
- expect(csp.send(:same_origin?)).to be true
112
-
113
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com:123'}, :request => request_for(FIREFOX, "https://example.com:123"))
114
- expect(csp.send(:same_origin?)).to be true
115
-
116
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com"))
117
- expect(csp.send(:same_origin?)).to be true
118
-
119
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com:80'}, :request => request_for(FIREFOX, "http://example.com"))
120
- expect(csp.send(:same_origin?)).to be true
121
-
122
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:80"))
123
- expect(csp.send(:same_origin?)).to be true
124
- end
125
-
126
- it "does not match port mismatches" do
127
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://example.com:81"))
128
- expect(csp.send(:same_origin?)).to be false
129
- end
130
-
131
- it "does not match host mismatches" do
132
- csp = ContentSecurityPolicy.new({:report_uri => 'http://twitter.com'}, :request => request_for(FIREFOX, "http://example.com"))
133
- expect(csp.send(:same_origin?)).to be false
134
- end
135
-
136
- it "does not match host mismatches because of subdomains" do
137
- csp = ContentSecurityPolicy.new({:report_uri => 'http://example.com'}, :request => request_for(FIREFOX, "http://sub.example.com"))
138
- expect(csp.send(:same_origin?)).to be false
139
- end
140
-
141
- it "does not match scheme mismatches" do
142
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "ftp://example.com"))
143
- expect(csp.send(:same_origin?)).to be false
144
- end
145
-
146
- it "does not match on substring collisions" do
147
- csp = ContentSecurityPolicy.new({:report_uri => 'https://example.com'}, :request => request_for(FIREFOX, "https://anotherexample.com"))
148
- expect(csp.send(:same_origin?)).to be false
149
- end
150
- end
151
-
152
- describe "#normalize_reporting_endpoint" do
153
- let(:opts) {{:report_uri => 'https://example.com/csp', :forward_endpoint => anything}}
154
-
155
- context "when using firefox" do
156
- it "updates the report-uri when posting to a different host" do
157
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://anexample.com"))
158
- expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
159
- end
160
-
161
- it "doesn't change report-uri if a path supplied" do
162
- csp = ContentSecurityPolicy.new({:report_uri => "/csp_reports"}, :request => request_for(FIREFOX, "https://anexample.com"))
163
- expect(csp.report_uri).to eq("/csp_reports")
164
- end
165
-
166
- it "forwards if the request_uri is set to a non-matching value" do
167
- csp = ContentSecurityPolicy.new({:report_uri => "https://another.example.com", :forward_endpoint => '/somewhere'}, :ua => "Firefox", :request_uri => "https://anexample.com")
168
- expect(csp.report_uri).to eq(FF_CSP_ENDPOINT)
169
- end
170
- end
171
-
172
- it "does not update the URI is the report_uri is on the same origin" do
173
- opts = {:report_uri => 'https://example.com/csp', :forward_endpoint => 'https://anotherexample.com'}
174
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, "https://example.com/somewhere"))
175
- expect(csp.report_uri).to eq('https://example.com/csp')
176
- end
177
-
178
- it "does not update the report-uri when using a non-firefox browser" do
179
- csp = ContentSecurityPolicy.new(opts, :request => request_for(CHROME))
180
- expect(csp.report_uri).to eq('https://example.com/csp')
181
- end
182
-
183
- context "when using a protocol-relative value for report-uri" do
184
- let(:opts) {
185
- {
186
- :default_src => 'self',
187
- :report_uri => '//example.com/csp'
102
+ :default_src => lambda { "http://lambda/result" },
103
+ :enforce => lambda { true },
104
+ :disable_fill_missing => lambda { true }
188
105
  }
189
- }
190
-
191
- it "uses the current protocol" do
192
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX, '/', :ssl => true))
193
- expect(csp.value).to match(%r{report-uri https://example.com/csp;})
194
-
195
- csp = ContentSecurityPolicy.new(opts, :request => request_for(FIREFOX))
196
- expect(csp.value).to match(%r{report-uri http://example.com/csp;})
197
- end
198
106
 
199
- it "uses the pre-configured https protocol" do
200
- csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => true)
201
- expect(csp.value).to match(%r{report-uri https://example.com/csp;})
202
- end
203
-
204
- it "uses the pre-configured http protocol" do
205
- csp = ContentSecurityPolicy.new(opts, :ua => "Firefox", :ssl => false)
206
- expect(csp.value).to match(%r{report-uri http://example.com/csp;})
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")
207
110
  end
208
111
  end
209
112
  end
@@ -213,17 +116,29 @@ module SecureHeaders
213
116
  csp = ContentSecurityPolicy.new({:script_src => 'anything'}, :request => request_for(CHROME))
214
117
  expect {
215
118
  csp.value
216
- }.to raise_error(ContentSecurityPolicyBuildError, "Couldn't build CSP header :( Expected to find default_src directive value")
119
+ }.to raise_error(RuntimeError)
120
+ end
121
+
122
+ context "CSP level 2 directives" do
123
+ let(:config) { {:default_src => 'self'} }
124
+ ::SecureHeaders::ContentSecurityPolicy::Constants::NON_DEFAULT_SOURCES.each do |non_default_source|
125
+ it "supports all level 2 directives" do
126
+ directive_name = ::SecureHeaders::ContentSecurityPolicy.send(:symbol_to_hyphen_case, non_default_source)
127
+ config.merge!({ non_default_source => "value" })
128
+ csp = ContentSecurityPolicy.new(config, :request => request_for(CHROME))
129
+ expect(csp.value).to match(/#{directive_name} value;/)
130
+ end
131
+ end
217
132
  end
218
133
 
219
134
  context "auto-whitelists data: uris for img-src" do
220
135
  it "sets the value if no img-src specified" do
221
- csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
222
- expect(csp.value).to eq("default-src 'self'; img-src data:;")
136
+ csp = ContentSecurityPolicy.new({:default_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
137
+ expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
223
138
  end
224
139
 
225
140
  it "appends the value if img-src is specified" do
226
- csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true, :disable_chrome_extension => true}, :request => request_for(CHROME))
141
+ csp = ContentSecurityPolicy.new({:default_src => 'self', :img_src => 'self', :disable_fill_missing => true}, :request => request_for(CHROME))
227
142
  expect(csp.value).to eq("default-src 'self'; img-src 'self' data:;")
228
143
  end
229
144
  end
@@ -231,7 +146,7 @@ module SecureHeaders
231
146
  it "fills in directives without values with default-src value" do
232
147
  options = default_opts.merge(:disable_fill_missing => false)
233
148
  csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
234
- 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;"
149
+ 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;"
235
150
  expect(csp.value).to eq(value)
236
151
  end
237
152
 
@@ -243,41 +158,39 @@ module SecureHeaders
243
158
  context "Firefox" do
244
159
  it "builds a csp header for firefox" do
245
160
  csp = ContentSecurityPolicy.new(default_opts, :request => request_for(FIREFOX))
246
- expect(csp.value).to eq("default-src https://*; img-src data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;")
161
+ 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;")
247
162
  end
248
163
  end
249
164
 
250
165
  context "Chrome" do
251
166
  it "builds a csp header for chrome" do
252
167
  csp = ContentSecurityPolicy.new(default_opts, :request => request_for(CHROME))
253
- expect(csp.value).to eq("default-src https://*; img-src data:; script-src 'unsafe-inline' 'unsafe-eval' https://* data:; style-src 'unsafe-inline' https://* about:; report-uri /csp_report;")
168
+ 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;")
254
169
  end
170
+ end
255
171
 
256
- it "ignores :forward_endpoint settings" do
257
- csp = ContentSecurityPolicy.new(@options_with_forwarding, :request => request_for(CHROME))
258
- expect(csp.value).to match(/report-uri #{@options_with_forwarding[:report_uri]};/)
172
+ context "when using a nonce" do
173
+ it "adds a nonce and unsafe-inline to the script-src value" do
174
+ header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce"), :request => request_for(CHROME))
175
+ expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
259
176
  end
260
- end
261
177
 
262
- context "when supplying a experimental values" do
263
- let(:options) {{
264
- :disable_chrome_extension => true,
265
- :disable_fill_missing => true,
266
- :default_src => 'self',
267
- :script_src => 'https://*',
268
- :experimental => {
269
- :script_src => 'self'
270
- }
271
- }}
178
+ it "adds a nonce and unsafe-inline to the style-src value" do
179
+ header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce"), :request => request_for(CHROME))
180
+ expect(header.value).to include("style-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline'")
181
+ end
272
182
 
273
- it "returns the original value" do
274
- header = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
275
- expect(header.value).to eq("default-src 'self'; img-src data:; script-src https://*;")
183
+ it "adds an identical nonce to the style and script-src directives" do
184
+ header = ContentSecurityPolicy.new(default_opts.merge(:style_src => "self nonce", :script_src => "self nonce"), :request => request_for(CHROME))
185
+ nonce = header.nonce
186
+ value = header.value
187
+ expect(value).to include("style-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
188
+ expect(value).to include("script-src 'self' 'nonce-#{nonce}' 'unsafe-inline'")
276
189
  end
277
190
 
278
- it "it returns the experimental value if requested" do
279
- header = ContentSecurityPolicy.new(options, {:request => request_for(CHROME), :experimental => true})
280
- expect(header.value).not_to match(/https/)
191
+ it "does not add 'unsafe-inline' twice" do
192
+ header = ContentSecurityPolicy.new(default_opts.merge(:script_src => "self nonce inline"), :request => request_for(CHROME))
193
+ expect(header.value).to include("script-src 'self' 'nonce-#{header.nonce}' 'unsafe-inline';")
281
194
  end
282
195
  end
283
196
 
@@ -285,15 +198,15 @@ module SecureHeaders
285
198
  let(:options) {
286
199
  default_opts.merge({
287
200
  :http_additions => {
288
- :frame_src => "http://*",
289
- :img_src => "http://*"
201
+ :frame_src => "http:",
202
+ :img_src => "http:"
290
203
  }
291
204
  })
292
205
  }
293
206
 
294
207
  it "adds directive values for headers on http" do
295
208
  csp = ContentSecurityPolicy.new(options, :request => request_for(CHROME))
296
- 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;")
209
+ 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;")
297
210
  end
298
211
 
299
212
  it "does not add the directive values if requesting https" do
@@ -305,45 +218,47 @@ module SecureHeaders
305
218
  csp = ContentSecurityPolicy.new(options, :ua => "Chrome", :ssl => true)
306
219
  expect(csp.value).not_to match(/http:/)
307
220
  end
221
+ end
308
222
 
309
- context "when supplying an experimental block" do
310
- # this simulates the situation where we are enforcing that scripts
311
- # only come from http[s]? depending if we're on ssl or not. The
312
- # report only tag will allow scripts from self over ssl, and
313
- # from a secure CDN over non-ssl
314
- let(:options) {{
315
- :disable_chrome_extension => true,
316
- :disable_fill_missing => true,
317
- :default_src => 'self',
318
- :script_src => 'https://*',
319
- :http_additions => {
320
- :script_src => 'http://*'
321
- },
322
- :experimental => {
323
- :script_src => 'self',
324
- :http_additions => {
325
- :script_src => 'https://mycdn.example.com'
326
- }
327
- }
328
- }}
329
- # for comparison purposes, if not using the experimental header this would produce
330
- # "allow 'self'; script-src https://*" for https requests
331
- # and
332
- # "allow 'self'; script-src https://* http://*" for http requests
333
-
334
- it "uses the value in the experimental block over SSL" do
335
- csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX, '/', :ssl => true))
336
- expect(csp.value).to eq("default-src 'self'; img-src data:; script-src 'self';")
223
+ describe "class methods" do
224
+ let(:ua) { CHROME }
225
+ let(:env) do
226
+ double.tap do |env|
227
+ allow(env).to receive(:[]).with('HTTP_USER_AGENT').and_return(ua)
337
228
  end
229
+ end
230
+ let(:request) do
231
+ double(
232
+ :ssl? => true,
233
+ :url => 'https://example.com',
234
+ :env => env
235
+ )
236
+ end
237
+
238
+ describe ".add_to_env" do
239
+ let(:controller) { double }
240
+ let(:config) { {:default_src => 'self'} }
241
+ let(:options) { {:controller => controller} }
338
242
 
339
- it "detects the :ssl => true option" do
340
- csp = ContentSecurityPolicy.new(options, :experimental => true, :ua => FIREFOX, :ssl => true)
341
- expect(csp.value).to eq("default-src 'self'; img-src data:; script-src 'self';")
243
+ it "adds metadata to env" do
244
+ metadata = {
245
+ :config => config,
246
+ :options => options
247
+ }
248
+ expect(ContentSecurityPolicy).to receive(:options_from_request).and_return(options)
249
+ expect(env).to receive(:[]=).with(ContentSecurityPolicy::ENV_KEY, metadata)
250
+ ContentSecurityPolicy.add_to_env(request, controller, config)
342
251
  end
252
+ end
343
253
 
344
- it "merges the values from experimental/http_additions when not over SSL" do
345
- csp = ContentSecurityPolicy.new(options, :experimental => true, :request => request_for(FIREFOX))
346
- expect(csp.value).to eq("default-src 'self'; img-src data:; script-src 'self' https://mycdn.example.com;")
254
+ describe ".options_from_request" do
255
+ it "extracts options from request" do
256
+ options = ContentSecurityPolicy.options_from_request(request)
257
+ expect(options).to eql({
258
+ :ua => ua,
259
+ :ssl => true,
260
+ :request_uri => 'https://example.com'
261
+ })
347
262
  end
348
263
  end
349
264
  end
@@ -10,6 +10,7 @@ module SecureHeaders
10
10
  specify { expect(StrictTransportSecurity.new(:max_age => '1234').value).to eq("max-age=1234")}
11
11
  specify { expect(StrictTransportSecurity.new(:max_age => 1234).value).to eq("max-age=1234")}
12
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")}
13
14
 
14
15
  context "with an invalid configuration" do
15
16
  context "with a hash argument" do
@@ -0,0 +1,32 @@
1
+ module SecureHeaders
2
+ describe XDownloadOptions do
3
+ specify { expect(XDownloadOptions.new.name).to eq(XDO_HEADER_NAME)}
4
+ specify { expect(XDownloadOptions.new.value).to eq("noopen")}
5
+ specify { expect(XDownloadOptions.new('noopen').value).to eq('noopen')}
6
+ specify { expect(XDownloadOptions.new(:value => 'noopen').value).to eq('noopen') }
7
+
8
+ context "invalid configuration values" do
9
+ it "accepts noopen" do
10
+ expect {
11
+ XDownloadOptions.new("noopen")
12
+ }.not_to raise_error
13
+
14
+ expect {
15
+ XDownloadOptions.new(:value => "noopen")
16
+ }.not_to raise_error
17
+ end
18
+
19
+ it "accepts nil" do
20
+ expect {
21
+ XDownloadOptions.new
22
+ }.not_to raise_error
23
+ end
24
+
25
+ it "doesn't accept anything besides noopen" do
26
+ expect {
27
+ XDownloadOptions.new("open")
28
+ }.to raise_error
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ module SecureHeaders
2
+ describe XPermittedCrossDomainPolicies do
3
+ specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)}
4
+ specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")}
5
+ specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')}
6
+ specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') }
7
+
8
+ context "valid configuration values" do
9
+ it "accepts 'all'" do
10
+ expect {
11
+ XPermittedCrossDomainPolicies.new("all")
12
+ }.not_to raise_error
13
+
14
+ expect {
15
+ XPermittedCrossDomainPolicies.new(:value => "all")
16
+ }.not_to raise_error
17
+ end
18
+
19
+ it "accepts 'by-ftp-filename'" do
20
+ expect {
21
+ XPermittedCrossDomainPolicies.new("by-ftp-filename")
22
+ }.not_to raise_error
23
+
24
+ expect {
25
+ XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename")
26
+ }.not_to raise_error
27
+ end
28
+
29
+ it "accepts 'by-content-type'" do
30
+ expect {
31
+ XPermittedCrossDomainPolicies.new("by-content-type")
32
+ }.not_to raise_error
33
+
34
+ expect {
35
+ XPermittedCrossDomainPolicies.new(:value => "by-content-type")
36
+ }.not_to raise_error
37
+ end
38
+ it "accepts 'master-only'" do
39
+ expect {
40
+ XPermittedCrossDomainPolicies.new("master-only")
41
+ }.not_to raise_error
42
+
43
+ expect {
44
+ XPermittedCrossDomainPolicies.new(:value => "master-only")
45
+ }.not_to raise_error
46
+ end
47
+
48
+ it "accepts nil" do
49
+ expect {
50
+ XPermittedCrossDomainPolicies.new
51
+ }.not_to raise_error
52
+ end
53
+ end
54
+
55
+ context 'invlaid configuration values' do
56
+
57
+ it "doesn't accept invalid values" do
58
+ expect {
59
+ XPermittedCrossDomainPolicies.new("open")
60
+ }.to raise_error
61
+ end
62
+ end
63
+ end
64
+ end
@@ -4,6 +4,7 @@ module SecureHeaders
4
4
  specify { expect(XXssProtection.new.value).to eq("1")}
5
5
  specify { expect(XXssProtection.new("0").value).to eq("0")}
6
6
  specify { expect(XXssProtection.new(:value => 1, :mode => 'block').value).to eq('1; mode=block') }
7
+ specify { expect(XXssProtection.new(:value => 1, :mode => 'block', :report_uri => 'https://www.secure.com/reports').value).to eq('1; mode=block; report=https://www.secure.com/reports') }
7
8
 
8
9
  context "with invalid configuration" do
9
10
  it "should raise an error when providing a string that is not valid" do
@@ -50,4 +51,4 @@ module SecureHeaders
50
51
 
51
52
  end
52
53
  end
53
- end
54
+ end