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