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
@@ -1,76 +1,160 @@
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
+ OTHER = [
35
+ :report_uri
36
+ ]
37
+
38
+ SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
39
+
40
+ ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
12
41
  end
13
42
  include Constants
14
43
 
15
- attr_accessor *META
16
- attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
17
-
18
- alias :disable_chrome_extension? :disable_chrome_extension
44
+ attr_reader :disable_fill_missing, :ssl_request
19
45
  alias :disable_fill_missing? :disable_fill_missing
20
46
  alias :ssl_request? :ssl_request
21
47
 
48
+ class << self
49
+ def generate_nonce
50
+ SecureRandom.base64(32).chomp
51
+ end
52
+
53
+ def set_nonce(controller, nonce = generate_nonce)
54
+ controller.instance_variable_set(:@content_security_policy_nonce, nonce)
55
+ end
56
+
57
+ def add_to_env(request, controller, config)
58
+ set_nonce(controller)
59
+ options = options_from_request(request).merge(:controller => controller)
60
+ request.env[Constants::ENV_KEY] = {
61
+ :config => config,
62
+ :options => options,
63
+ }
64
+ end
65
+
66
+ def options_from_request(request)
67
+ {
68
+ :ssl => request.ssl?,
69
+ :ua => request.env['HTTP_USER_AGENT'],
70
+ :request_uri => request_uri_from_request(request),
71
+ }
72
+ end
73
+
74
+ def request_uri_from_request(request)
75
+ if request.respond_to?(:original_url)
76
+ # rails 3.1+
77
+ request.original_url
78
+ else
79
+ # rails 2/3.0
80
+ request.url
81
+ end
82
+ end
83
+
84
+ def symbol_to_hyphen_case sym
85
+ sym.to_s.gsub('_', '-')
86
+ end
87
+ end
88
+
22
89
  # +options+ param contains
23
- # :experimental use experimental block for config
90
+ # :controller used for setting instance variables for nonces/hashes
24
91
  # :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
92
  # :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
28
93
  #
29
94
  # :report used to determine what :ssl_request, :ua, and :request_uri are set to
30
95
  def initialize(config=nil, options={})
31
- @experimental = !!options.delete(:experimental)
32
- @controller = options.delete(:controller)
96
+ return unless config
97
+
33
98
  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)
99
+ options = options.merge(self.class.options_from_request(options[:request]))
43
100
  end
44
101
 
45
- configure(config) if config
46
- end
47
-
48
- def configure(config)
49
- @config = config.dup
102
+ @controller = options[:controller]
103
+ @ua = options[:ua]
104
+ @ssl_request = !!options.delete(:ssl)
105
+ @request_uri = options.delete(:request_uri)
106
+
107
+ # Config values can be string, array, or lamdba values
108
+ @config = config.inject({}) do |hash, (key, value)|
109
+ config_val = value.respond_to?(:call) ? value.call : value
110
+
111
+ if SOURCE_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
112
+ config_val = config_val.split if config_val.is_a? String
113
+ if config_val.is_a?(Array)
114
+ config_val = config_val.map do |val|
115
+ translate_dir_value(val)
116
+ end.flatten.uniq
117
+ end
118
+ end
50
119
 
51
- experimental_config = @config.delete(:experimental)
52
- if @experimental && experimental_config
53
- @config[:http_additions] = experimental_config[:http_additions]
54
- @config.merge!(experimental_config)
120
+ hash[key] = config_val
121
+ hash
55
122
  end
56
123
 
57
- META.each do |meta|
58
- self.send("#{meta}=", @config.delete(meta))
59
- end
124
+ @http_additions = @config.delete(:http_additions)
125
+ @app_name = @config.delete(:app_name)
126
+ @report_uri = @config.delete(:report_uri)
127
+
128
+ @disable_fill_missing = !!@config.delete(:disable_fill_missing)
129
+ @enforce = !!@config.delete(:enforce)
130
+ @tag_report_uri = !!@config.delete(:tag_report_uri)
131
+ @script_hashes = @config.delete(:script_hashes) || []
60
132
 
61
- normalize_csp_options
62
- normalize_reporting_endpoint
133
+ add_script_hashes if @script_hashes.any?
63
134
  fill_directives unless disable_fill_missing?
64
135
  end
65
136
 
137
+ ##
138
+ # Return or initialize the nonce value used for this header.
139
+ # If a reference to a controller is passed in the config, this method
140
+ # will check if a nonce has already been set and use it.
141
+ def nonce
142
+ @nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce
143
+ end
144
+
145
+ ##
146
+ # Returns the name to use for the header. Either "Content-Security-Policy" or
147
+ # "Content-Security-Policy-Report-Only"
66
148
  def name
67
- base = STANDARD_HEADER_NAME
68
- if !enforce || experimental
149
+ base = HEADER_NAME
150
+ if !@enforce
69
151
  base += "-Report-Only"
70
152
  end
71
153
  base
72
154
  end
73
155
 
156
+ ##
157
+ # Return the value of the CSP header
74
158
  def value
75
159
  return @config if @config.is_a?(String)
76
160
  if @config
@@ -82,97 +166,52 @@ module SecureHeaders
82
166
 
83
167
  private
84
168
 
169
+ def add_script_hashes
170
+ @config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
171
+ end
172
+
85
173
  def build_value
86
174
  raise "Expected to find default_src directive value" unless @config[:default_src]
87
175
  append_http_additions unless ssl_request?
88
176
  header_value = [
89
- # ensure default-src is first
90
- build_directive(:default_src),
91
- generic_directives(@config),
177
+ generic_directives,
178
+ non_default_directives,
92
179
  report_uri_directive
93
- ].join
94
-
95
- #store the value for next time
96
- @config = header_value
97
- header_value.strip
98
- rescue StandardError => e
99
- raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
180
+ ].join.strip
100
181
  end
101
182
 
102
183
  def fill_directives
103
- return unless @config[:default_src]
104
- default = @config[:default_src]
105
- DIRECTIVES.each do |directive|
106
- unless @config[directive]
107
- @config[directive] = default
184
+ if default = @config[:default_src]
185
+ DIRECTIVES.each do |directive|
186
+ unless @config[directive]
187
+ @config[directive] = default
188
+ end
108
189
  end
109
190
  end
110
- @config
111
191
  end
112
192
 
113
193
  def append_http_additions
114
- return unless http_additions
115
- http_additions.each do |k, v|
194
+ return unless @http_additions
195
+ @http_additions.each do |k, v|
116
196
  @config[k] ||= []
117
197
  @config[k] << v
118
198
  end
119
199
  end
120
200
 
121
- def normalize_csp_options
122
- @config = @config.inject({}) do |hash, (k, v)|
123
- config_val = if v.respond_to?(:call)
124
- v.call
125
- else
126
- v
127
- end
128
-
129
- config_val = config_val.split if config_val.is_a? String
130
- config_val = config_val.map do |val|
131
- translate_dir_value(val)
132
- end
133
-
134
- hash[k] = config_val
135
- hash
136
- end
137
-
138
- @report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
139
- end
140
-
141
- # translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
142
201
  def translate_dir_value val
143
202
  if %w{inline eval}.include?(val)
144
203
  val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
145
- # self/none are special sources/src-dir-values and need to be quoted in chrome
204
+ # self/none are special sources/src-dir-values and need to be quoted
146
205
  elsif %{self none}.include?(val)
147
206
  "'#{val}'"
207
+ elsif val == 'nonce'
208
+ self.class.set_nonce(@controller, nonce)
209
+ ["'nonce-#{nonce}'", "'unsafe-inline'"]
148
210
  else
149
211
  val
150
212
  end
151
213
  end
152
214
 
153
- # if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
154
- # or only a path was supplied (in which case we assume cross-host)
155
- # we need to forward the request for Firefox.
156
- def normalize_reporting_endpoint
157
- if @ua && @ua =~ /Firefox/
158
- if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
159
- return
160
- end
161
-
162
- if forward_endpoint
163
- @report_uri = FF_CSP_ENDPOINT
164
- end
165
- end
166
- end
167
-
168
- def same_origin?
169
- return unless report_uri && request_uri
170
-
171
- origin = URI.parse(request_uri)
172
- uri = URI.parse(report_uri)
173
- uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
174
- end
175
-
176
215
  def report_uri_directive
177
216
  return '' if @report_uri.nil?
178
217
 
@@ -184,43 +223,40 @@ module SecureHeaders
184
223
  end
185
224
  end
186
225
 
226
+ if @tag_report_uri
227
+ @report_uri = "#{@report_uri}?enforce=#{@enforce}"
228
+ @report_uri += "&app_name=#{@app_name}" if @app_name
229
+ end
230
+
187
231
  "report-uri #{@report_uri};"
188
232
  end
189
233
 
190
- def generic_directives(config)
234
+ def generic_directives
191
235
  header_value = ''
192
- if config[:img_src]
193
- config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
236
+ if @config[:img_src]
237
+ @config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:')
194
238
  else
195
- config[:img_src] = ['data:']
239
+ @config[:img_src] = @config[:default_src] + ['data:']
196
240
  end
197
241
 
198
- config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
199
- header_value += build_directive(k)
242
+ DIRECTIVES.each do |directive_name|
243
+ header_value += build_directive(directive_name) if @config[directive_name]
200
244
  end
201
245
 
202
246
  header_value
203
247
  end
204
248
 
205
- # build and deletes the directive
206
- def build_directive(key)
207
- "#{symbol_to_hyphen_case(key)} #{@config.delete(key).join(" ")}; "
208
- end
249
+ def non_default_directives
250
+ header_value = ''
251
+ NON_DEFAULT_SOURCES.each do |directive_name|
252
+ header_value += build_directive(directive_name) if @config[directive_name]
253
+ end
209
254
 
210
- def symbol_to_hyphen_case sym
211
- sym.to_s.gsub('_', '-')
255
+ header_value
212
256
  end
213
257
 
214
- def parse_request request
215
- @ssl_request = request.ssl?
216
- @ua = request.env['HTTP_USER_AGENT']
217
- @request_uri = if request.respond_to?(:original_url)
218
- # rails 3.1+
219
- request.original_url
220
- else
221
- # rails 2/3.0
222
- request.url
223
- end
258
+ def build_directive(key)
259
+ "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
224
260
  end
225
261
  end
226
262
  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.2.0"
2
+ VERSION = "2.0.2"
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
+ class Base #:nodoc:
66
+ include SecureHeaders::ViewHelpers
67
+ end
68
+ end