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
@@ -1,79 +1,154 @@
1
1
  require 'uri'
2
+ require 'base64'
3
+ require 'securerandom'
2
4
 
3
5
  module SecureHeaders
4
6
  class ContentSecurityPolicyBuildError < StandardError; end
5
7
  class ContentSecurityPolicy < Header
6
8
  module Constants
7
- DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https://* about: javascript:; img-src data:"
8
- STANDARD_HEADER_NAME = "Content-Security-Policy"
9
- FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
10
- DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
11
- META = [:enforce, :http_additions, :disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
9
+ DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
10
+ HEADER_NAME = "Content-Security-Policy"
11
+ ENV_KEY = 'secure_headers.content_security_policy'
12
+ DIRECTIVES = [
13
+ :default_src,
14
+ :connect_src,
15
+ :font_src,
16
+ :frame_src,
17
+ :img_src,
18
+ :media_src,
19
+ :object_src,
20
+ :script_src,
21
+ :style_src
22
+ ]
23
+
24
+ NON_DEFAULT_SOURCES = [
25
+ :base_uri,
26
+ :child_src,
27
+ :form_action,
28
+ :frame_ancestors,
29
+ :plugin_types,
30
+ :referrer,
31
+ :reflected_xss
32
+ ]
33
+
34
+ ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
12
35
  end
13
36
  include Constants
14
37
 
15
- attr_accessor *META
16
- attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
17
-
18
- alias :disable_chrome_extension? :disable_chrome_extension
38
+ attr_reader :disable_fill_missing, :ssl_request
19
39
  alias :disable_fill_missing? :disable_fill_missing
20
40
  alias :ssl_request? :ssl_request
21
41
 
42
+ class << self
43
+ def generate_nonce
44
+ SecureRandom.base64(32).chomp
45
+ end
46
+
47
+ def set_nonce(controller, nonce = generate_nonce)
48
+ controller.instance_variable_set(:@content_security_policy_nonce, nonce)
49
+ end
50
+
51
+ def add_to_env(request, controller, config)
52
+ set_nonce(controller)
53
+ options = options_from_request(request).merge(:controller => controller)
54
+ request.env[Constants::ENV_KEY] = {
55
+ :config => config,
56
+ :options => options,
57
+ }
58
+ end
59
+
60
+ def options_from_request(request)
61
+ {
62
+ :ssl => request.ssl?,
63
+ :ua => request.env['HTTP_USER_AGENT'],
64
+ :request_uri => request_uri_from_request(request),
65
+ }
66
+ end
67
+
68
+ def request_uri_from_request(request)
69
+ if request.respond_to?(:original_url)
70
+ # rails 3.1+
71
+ request.original_url
72
+ else
73
+ # rails 2/3.0
74
+ request.url
75
+ end
76
+ end
77
+
78
+ def symbol_to_hyphen_case sym
79
+ sym.to_s.gsub('_', '-')
80
+ end
81
+ end
82
+
22
83
  # +options+ param contains
23
- # :experimental use experimental block for config
84
+ # :controller used for setting instance variables for nonces/hashes
24
85
  # :ssl_request used to determine if http_additions should be used
25
- # :request_uri used to determine if firefox should send the report directly
26
- # or use the forwarding endpoint
27
86
  # :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
28
87
  #
29
88
  # :report used to determine what :ssl_request, :ua, and :request_uri are set to
30
89
  def initialize(config=nil, options={})
31
- @experimental = !!options.delete(:experimental)
32
- @controller = options.delete(:controller)
90
+ return unless config
91
+
33
92
  if options[:request]
34
- parse_request(options[:request])
35
- else
36
- @ua = options[:ua]
37
- # fails open, assumes http. Bad idea? Will always include http additions.
38
- # could also fail if not supplied.
39
- @ssl_request = !!options.delete(:ssl)
40
- # a nil value here means we always assume we are not on the same host,
41
- # which causes all FF csp reports to go through the forwarder
42
- @request_uri = options.delete(:request_uri)
93
+ options = options.merge(self.class.options_from_request(options[:request]))
43
94
  end
44
95
 
45
- configure(config) if config
46
- end
47
-
48
- def configure(config)
49
- @config = config.dup
50
-
51
- experimental_config = @config.delete(:experimental)
52
- if @experimental && experimental_config
53
- @config[:http_additions] = experimental_config[:http_additions]
54
- @config.merge!(experimental_config)
55
- end
96
+ @controller = options[:controller]
97
+ @ua = options[:ua]
98
+ @ssl_request = !!options.delete(:ssl)
99
+ @request_uri = options.delete(:request_uri)
100
+
101
+ # Config values can be string, array, or lamdba values
102
+ @config = config.inject({}) do |hash, (key, value)|
103
+ config_val = value.respond_to?(:call) ? value.call : value
104
+
105
+ if ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
106
+ config_val = config_val.split if config_val.is_a? String
107
+ if config_val.is_a?(Array)
108
+ config_val = config_val.map do |val|
109
+ translate_dir_value(val)
110
+ end.flatten.uniq
111
+ end
112
+ end
56
113
 
57
- META.each do |meta|
58
- self.send("#{meta}=", @config.delete(meta))
114
+ hash[key] = config_val
115
+ hash
59
116
  end
60
117
 
118
+ @http_additions = @config.delete(:http_additions)
119
+ @app_name = @config.delete(:app_name)
61
120
  @report_uri = @config.delete(:report_uri)
62
- @script_nonce = @config.delete(:script_nonce)
63
121
 
64
- normalize_csp_options
65
- normalize_reporting_endpoint
122
+ @disable_fill_missing = !!@config.delete(:disable_fill_missing)
123
+ @enforce = !!@config.delete(:enforce)
124
+ @tag_report_uri = !!@config.delete(:tag_report_uri)
125
+ @script_hashes = @config.delete(:script_hashes) || []
126
+
127
+ add_script_hashes if @script_hashes.any?
66
128
  fill_directives unless disable_fill_missing?
67
129
  end
68
130
 
131
+ ##
132
+ # Return or initialize the nonce value used for this header.
133
+ # If a reference to a controller is passed in the config, this method
134
+ # will check if a nonce has already been set and use it.
135
+ def nonce
136
+ @nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce
137
+ end
138
+
139
+ ##
140
+ # Returns the name to use for the header. Either "Content-Security-Policy" or
141
+ # "Content-Security-Policy-Report-Only"
69
142
  def name
70
- base = STANDARD_HEADER_NAME
71
- if !enforce || experimental
143
+ base = HEADER_NAME
144
+ if !@enforce
72
145
  base += "-Report-Only"
73
146
  end
74
147
  base
75
148
  end
76
149
 
150
+ ##
151
+ # Return the value of the CSP header
77
152
  def value
78
153
  return @config if @config.is_a?(String)
79
154
  if @config
@@ -85,87 +160,52 @@ module SecureHeaders
85
160
 
86
161
  private
87
162
 
163
+ def add_script_hashes
164
+ @config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
165
+ end
166
+
88
167
  def build_value
89
168
  raise "Expected to find default_src directive value" unless @config[:default_src]
90
169
  append_http_additions unless ssl_request?
91
170
  header_value = [
92
- # ensure default-src is first
93
- build_directive(:default_src),
94
- generic_directives(@config),
95
- report_uri_directive,
96
- script_nonce_directive,
97
- ].join
98
-
99
- #store the value for next time
100
- @config = header_value
101
- header_value.strip
102
- rescue StandardError => e
103
- raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
171
+ generic_directives,
172
+ non_default_directives,
173
+ report_uri_directive
174
+ ].join.strip
104
175
  end
105
176
 
106
177
  def fill_directives
107
- return unless @config[:default_src]
108
- default = @config[:default_src]
109
- DIRECTIVES.each do |directive|
110
- unless @config[directive]
111
- @config[directive] = default
178
+ if default = @config[:default_src]
179
+ DIRECTIVES.each do |directive|
180
+ unless @config[directive]
181
+ @config[directive] = default
182
+ end
112
183
  end
113
184
  end
114
- @config
115
185
  end
116
186
 
117
187
  def append_http_additions
118
- return unless http_additions
119
- http_additions.each do |k, v|
188
+ return unless @http_additions
189
+ @http_additions.each do |k, v|
120
190
  @config[k] ||= []
121
191
  @config[k] << v
122
192
  end
123
193
  end
124
194
 
125
- def normalize_csp_options
126
- @config.each do |k,v|
127
- @config[k] = v.split if v.is_a? String
128
- @config[k] = @config[k].map do |val|
129
- translate_dir_value(val)
130
- end
131
- end
132
- end
133
-
134
- # translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
135
195
  def translate_dir_value val
136
196
  if %w{inline eval}.include?(val)
137
197
  val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
138
- # self/none are special sources/src-dir-values and need to be quoted in chrome
198
+ # self/none are special sources/src-dir-values and need to be quoted
139
199
  elsif %{self none}.include?(val)
140
200
  "'#{val}'"
201
+ elsif val == 'nonce'
202
+ self.class.set_nonce(@controller, nonce)
203
+ ["'nonce-#{nonce}'", "'unsafe-inline'"]
141
204
  else
142
205
  val
143
206
  end
144
207
  end
145
208
 
146
- # if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
147
- # or only a path was supplied (in which case we assume cross-host)
148
- # we need to forward the request for Firefox.
149
- def normalize_reporting_endpoint
150
- if @ua && @ua =~ /Firefox/
151
- if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
152
- return
153
- end
154
-
155
- if forward_endpoint
156
- @report_uri = FF_CSP_ENDPOINT
157
- end
158
- end
159
- end
160
-
161
- def same_origin?
162
- return unless report_uri && request_uri
163
-
164
- origin = URI.parse(request_uri)
165
- uri = URI.parse(report_uri)
166
- uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
167
- end
168
-
169
209
  def report_uri_directive
170
210
  return '' if @report_uri.nil?
171
211
 
@@ -177,55 +217,40 @@ module SecureHeaders
177
217
  end
178
218
  end
179
219
 
180
- "report-uri #{@report_uri};"
181
- end
220
+ if @tag_report_uri
221
+ @report_uri = "#{@report_uri}?enforce=#{@enforce}"
222
+ @report_uri += "&app_name=#{@app_name}" if @app_name
223
+ end
182
224
 
183
- def script_nonce_directive
184
- return '' if @script_nonce.nil?
185
- nonce_value = if @script_nonce.is_a?(String)
186
- @script_nonce
187
- elsif @controller
188
- @controller.instance_exec(&@script_nonce)
189
- else
190
- @script_nonce.call
191
- end
192
- "script-nonce #{nonce_value};"
225
+ "report-uri #{@report_uri};"
193
226
  end
194
227
 
195
- def generic_directives(config)
228
+ def generic_directives
196
229
  header_value = ''
197
- if config[:img_src]
198
- config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
230
+ if @config[:img_src]
231
+ @config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:')
199
232
  else
200
- config[:img_src] = ['data:']
233
+ @config[:img_src] = @config[:default_src] + ['data:']
201
234
  end
202
235
 
203
- config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
204
- header_value += build_directive(k)
236
+ DIRECTIVES.each do |directive_name|
237
+ header_value += build_directive(directive_name) if @config[directive_name]
205
238
  end
206
239
 
207
240
  header_value
208
241
  end
209
242
 
210
- # build and deletes the directive
211
- def build_directive(key)
212
- "#{symbol_to_hyphen_case(key)} #{@config.delete(key).join(" ")}; "
213
- end
243
+ def non_default_directives
244
+ header_value = ''
245
+ NON_DEFAULT_SOURCES.each do |directive_name|
246
+ header_value += build_directive(directive_name) if @config[directive_name]
247
+ end
214
248
 
215
- def symbol_to_hyphen_case sym
216
- sym.to_s.gsub('_', '-')
249
+ header_value
217
250
  end
218
251
 
219
- def parse_request request
220
- @ssl_request = request.ssl?
221
- @ua = request.env['HTTP_USER_AGENT']
222
- @request_uri = if request.respond_to?(:original_url)
223
- # rails 3.1+
224
- request.original_url
225
- else
226
- # rails 2/3.0
227
- request.url
228
- end
252
+ def build_directive(key)
253
+ "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
229
254
  end
230
255
  end
231
256
  end
@@ -6,7 +6,7 @@ module SecureHeaders
6
6
  HSTS_HEADER_NAME = 'Strict-Transport-Security'
7
7
  HSTS_MAX_AGE = "631138519"
8
8
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
9
- VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?\z/i
9
+ VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
10
10
  MESSAGE = "The config value supplied for the HSTS header was invalid."
11
11
  end
12
12
  include Constants
@@ -31,6 +31,7 @@ module SecureHeaders
31
31
  max_age = @config.fetch(:max_age, HSTS_MAX_AGE)
32
32
  value = "max-age=" + max_age.to_s
33
33
  value += "; includeSubdomains" if @config[:include_subdomains]
34
+ value += "; preload" if @config[:preload]
34
35
 
35
36
  value
36
37
  end
@@ -0,0 +1,39 @@
1
+ module SecureHeaders
2
+ class XDOBuildError < StandardError; end
3
+ class XDownloadOptions < Header
4
+ module Constants
5
+ XDO_HEADER_NAME = "X-Download-Options"
6
+ DEFAULT_VALUE = 'noopen'
7
+ end
8
+ include Constants
9
+
10
+ def initialize(config = nil)
11
+ @config = config
12
+ validate_config unless @config.nil?
13
+ end
14
+
15
+ def name
16
+ XDO_HEADER_NAME
17
+ end
18
+
19
+ def value
20
+ case @config
21
+ when NilClass
22
+ DEFAULT_VALUE
23
+ when String
24
+ @config
25
+ else
26
+ @config[:value]
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def validate_config
33
+ value = @config.is_a?(Hash) ? @config[:value] : @config
34
+ unless value.casecmp(DEFAULT_VALUE) == 0
35
+ raise XDOBuildError.new("Value can only be nil or 'noopen'")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ module SecureHeaders
2
+ class XPCDPBuildError < StandardError; end
3
+ class XPermittedCrossDomainPolicies < Header
4
+ module Constants
5
+ XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
6
+ DEFAULT_VALUE = 'none'
7
+ VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
8
+ end
9
+ include Constants
10
+
11
+ def initialize(config = nil)
12
+ @config = config
13
+ validate_config unless @config.nil?
14
+ end
15
+
16
+ def name
17
+ XPCDP_HEADER_NAME
18
+ end
19
+
20
+ def value
21
+ case @config
22
+ when NilClass
23
+ DEFAULT_VALUE
24
+ when String
25
+ @config
26
+ else
27
+ @config[:value]
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def validate_config
34
+ value = @config.is_a?(Hash) ? @config[:value] : @config
35
+ unless VALID_POLICIES.include?(value.downcase)
36
+ raise XPCDPBuildError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -4,7 +4,7 @@ module SecureHeaders
4
4
  module Constants
5
5
  X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection'
6
6
  DEFAULT_VALUE = "1"
7
- VALID_X_XSS_HEADER = /\A[01](; mode=block)?\z/i
7
+ VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
8
8
  end
9
9
  include Constants
10
10
 
@@ -26,6 +26,7 @@ module SecureHeaders
26
26
  else
27
27
  value = @config[:value].to_s
28
28
  value += "; mode=#{@config[:mode]}" if @config[:mode]
29
+ value += "; report=#{@config[:report_uri]}" if @config[:report_uri]
29
30
  value
30
31
  end
31
32
  end
@@ -0,0 +1,14 @@
1
+ module SecureHeaders
2
+ module Padrino
3
+ class << self
4
+ ##
5
+ # Main class that register this extension.
6
+ #
7
+ def registered(app)
8
+ app.extend SecureHeaders::ClassMethods
9
+ app.helpers SecureHeaders::InstanceMethods
10
+ end
11
+ alias :included :registered
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,24 @@ if defined?(Rails::Railtie)
3
3
  module SecureHeaders
4
4
  class Railtie < Rails::Engine
5
5
  isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0
6
- ActionController::Base.send :include, ::SecureHeaders
6
+ conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options',
7
+ 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
+ 'X-Content-Type-Options', 'Strict-Transport-Security',
9
+ 'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
+ 'X-Permitted-Cross-Domain-Policies','Public-Key-Pins','Public-Key-Pins-Report-Only']
11
+
12
+ initializer "secure_headers.action_controller" do
13
+ ActiveSupport.on_load(:action_controller) do
14
+ include ::SecureHeaders
15
+
16
+ unless Rails.application.config.action_dispatch.default_headers.nil?
17
+ conflicting_headers.each do |header|
18
+ Rails.application.config.action_dispatch.default_headers.delete(header)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
7
24
  end
8
25
  end
9
26
  else
@@ -12,26 +29,4 @@ else
12
29
  include ::SecureHeaders
13
30
  end
14
31
  end
15
-
16
- module SecureHeaders
17
- module Routing
18
- module MapperExtensions
19
- def csp_endpoint
20
- @set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
21
- end
22
- end
23
- end
24
- end
25
-
26
- if defined?(ActiveSupport::Dependencies)
27
- if ActiveSupport::Dependencies.autoload_paths
28
- ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
29
- else
30
- ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
31
- end
32
- end
33
-
34
- if defined? ActionController::Routing
35
- ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
36
- end
37
- end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "1.1.1"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -0,0 +1,68 @@
1
+ module SecureHeaders
2
+ class UnexpectedHashedScriptException < StandardError
3
+
4
+ end
5
+
6
+ module ViewHelpers
7
+ include SecureHeaders::HashHelper
8
+ SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
9
+
10
+ def nonced_style_tag(content = nil, &block)
11
+ nonced_tag(content, :style, block)
12
+ end
13
+
14
+ def nonced_javascript_tag(content = nil, &block)
15
+ nonced_tag(content, :script, block)
16
+ end
17
+
18
+ def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block)
19
+ content = capture(&block)
20
+
21
+ if ['development', 'test'].include?(ENV["RAILS_ENV"])
22
+ hash_value = hash_source(content)
23
+ file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
24
+ script_hashes = controller.instance_variable_get(:@script_hashes)[file_path]
25
+ unless script_hashes && script_hashes.include?(hash_value)
26
+ message = unexpected_hash_error_message(file_path, hash_value, content)
27
+ if raise_error_on_unrecognized_hash
28
+ raise UnexpectedHashedScriptException.new(message)
29
+ else
30
+ puts message
31
+ request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
32
+ end
33
+ end
34
+ end
35
+
36
+ content_tag :script, content
37
+ end
38
+
39
+ private
40
+
41
+ def nonced_tag(content, type, block)
42
+ content = if block
43
+ capture(&block)
44
+ else
45
+ content.html_safe # :'(
46
+ end
47
+
48
+ content_tag type, content, :nonce => @content_security_policy_nonce
49
+ end
50
+
51
+ def unexpected_hash_error_message(file_path, hash_value, content)
52
+ <<-EOF
53
+ \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
54
+ <script>#{content}</script>
55
+ *** This is fine in dev/test, but will raise exceptions in production. ***
56
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
57
+ #{file_path}:
58
+ - #{hash_value}\n\n
59
+ EOF
60
+ end
61
+ end
62
+ end
63
+
64
+ module ActionView #:nodoc:
65
+ module Helpers #:nodoc:
66
+ include SecureHeaders::ViewHelpers
67
+ end
68
+ end