secure_headers 1.0.0 → 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 (129) 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 -8
  6. data/README.md +208 -134
  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 -16
  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 +160 -161
  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 +70 -29
  74. data/lib/tasks/tasks.rake +48 -0
  75. data/secure_headers.gemspec +4 -5
  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 -299
  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 -13
  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 +68 -83
  85. data/spec/spec_helper.rb +29 -18
  86. metadata +51 -66
  87. data/.rvmrc +0 -1
  88. data/Guardfile +0 -10
  89. data/HISTORY.md +0 -100
  90. data/app/controllers/content_security_policy_controller.rb +0 -53
  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/lib/secure_headers/headers/content_security_policy/browser_strategy.rb +0 -78
  126. data/lib/secure_headers/headers/content_security_policy/firefox_browser_strategy.rb +0 -72
  127. data/lib/secure_headers/headers/content_security_policy/ie_browser_strategy.rb +0 -6
  128. data/lib/secure_headers/headers/content_security_policy/standard_browser_strategy.rb +0 -22
  129. data/spec/controllers/content_security_policy_controller_spec.rb +0 -74
@@ -0,0 +1,7 @@
1
+ module SecureHeaders
2
+ module HashHelper
3
+ def hash_source(inline_script, digest = :SHA256)
4
+ [digest.to_s.downcase, "-", [[Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp].join
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module SecureHeaders
2
+ class ContentSecurityPolicy
3
+ class ScriptHashMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ status, headers, response = @app.call(env)
10
+ metadata = env[ContentSecurityPolicy::ENV_KEY]
11
+ if !metadata.nil?
12
+ config, options = metadata.values_at(:config, :options)
13
+ config.merge!(:script_hashes => env[HASHES_ENV_KEY])
14
+ csp_header = ContentSecurityPolicy.new(config, options)
15
+ headers[csp_header.name] = csp_header.value
16
+ end
17
+
18
+ [status, headers, response]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,202 +1,211 @@
1
1
  require 'uri'
2
- require 'brwsr'
2
+ require 'base64'
3
+ require 'securerandom'
3
4
 
4
5
  module SecureHeaders
5
6
  class ContentSecurityPolicyBuildError < StandardError; end
6
7
  class ContentSecurityPolicy < Header
7
8
  module Constants
8
- WEBKIT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https://* about: javascript:; img-src chrome-extension:"
9
- FIREFOX_CSP_HEADER = "options eval-script inline-script; allow https://* data:; frame-src https://* about: javascript:; img-src chrome-extension:"
10
-
11
- FIREFOX_CSP_HEADER_NAME = 'X-Content-Security-Policy'
12
- WEBKIT_CSP_HEADER_NAME = 'X-WebKit-CSP'
13
- STANDARD_HEADER_NAME = "Content-Security-Policy"
14
-
15
- FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
16
- WEBKIT_DIRECTIVES = DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
17
- FIREFOX_DIRECTIVES = DIRECTIVES + [:xhr_src, :frame_ancestors] - [:connect_src]
18
- 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
19
35
  end
20
36
  include Constants
21
37
 
22
- attr_accessor *META
23
- attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental, :config
24
-
25
- alias :disable_chrome_extension? :disable_chrome_extension
38
+ attr_reader :disable_fill_missing, :ssl_request
26
39
  alias :disable_fill_missing? :disable_fill_missing
27
40
  alias :ssl_request? :ssl_request
28
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
+
29
83
  # +options+ param contains
30
- # :experimental use experimental block for config
84
+ # :controller used for setting instance variables for nonces/hashes
31
85
  # :ssl_request used to determine if http_additions should be used
32
- # :request_uri used to determine if firefox should send the report directly
33
- # or use the forwarding endpoint
34
86
  # :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
35
87
  #
36
88
  # :report used to determine what :ssl_request, :ua, and :request_uri are set to
37
89
  def initialize(config=nil, options={})
38
- @experimental = !!options.delete(:experimental)
39
- @controller = options.delete(:controller)
90
+ return unless config
91
+
40
92
  if options[:request]
41
- parse_request(options[:request])
42
- else
43
- @browser = Brwsr::Browser.new(:ua => options[:ua])
44
- # fails open, assumes http. Bad idea? Will always include http additions.
45
- # could also fail if not supplied.
46
- @ssl_request = !!options.delete(:ssl)
47
- # a nil value here means we always assume we are not on the same host,
48
- # which causes all FF csp reports to go through the forwarder
49
- @request_uri = options.delete(:request_uri)
93
+ options = options.merge(self.class.options_from_request(options[:request]))
50
94
  end
51
95
 
52
- configure(config) if config
53
- end
54
-
55
- def configure opts
56
- @config = opts.dup
57
-
58
- experimental_config = @config.delete(:experimental)
59
- if @experimental && experimental_config
60
- @config[:http_additions] = experimental_config[:http_additions]
61
- @config.merge!(experimental_config)
62
- 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
63
113
 
64
- META.each do |meta|
65
- self.send("#{meta}=", @config.delete(meta))
114
+ hash[key] = config_val
115
+ hash
66
116
  end
67
117
 
118
+ @http_additions = @config.delete(:http_additions)
119
+ @app_name = @config.delete(:app_name)
68
120
  @report_uri = @config.delete(:report_uri)
69
- @script_nonce = @config.delete(:script_nonce)
70
121
 
71
- normalize_csp_options
72
- normalize_reporting_endpoint
73
- filter_unsupported_directives
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?
128
+ fill_directives unless disable_fill_missing?
129
+ end
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
74
137
  end
75
138
 
139
+ ##
140
+ # Returns the name to use for the header. Either "Content-Security-Policy" or
141
+ # "Content-Security-Policy-Report-Only"
76
142
  def name
77
- browser_strategy.name
143
+ base = HEADER_NAME
144
+ if !@enforce
145
+ base += "-Report-Only"
146
+ end
147
+ base
78
148
  end
79
149
 
150
+ ##
151
+ # Return the value of the CSP header
80
152
  def value
81
153
  return @config if @config.is_a?(String)
82
-
83
154
  if @config
84
155
  build_value
85
156
  else
86
- browser_strategy.csp_header
157
+ DEFAULT_CSP_HEADER
87
158
  end
88
159
  end
89
160
 
90
161
  private
91
162
 
92
- def browser_strategy
93
- @browser_strategy ||= BrowserStrategy.build(self)
94
- end
95
-
96
- def directives
97
- browser_strategy.directives
163
+ def add_script_hashes
164
+ @config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
98
165
  end
99
166
 
100
167
  def build_value
101
- fill_directives unless disable_fill_missing?
102
- browser_strategy.add_missing_extension_values unless disable_chrome_extension?
168
+ raise "Expected to find default_src directive value" unless @config[:default_src]
103
169
  append_http_additions unless ssl_request?
104
-
105
170
  header_value = [
106
- build_impl_specific_directives,
107
- generic_directives(@config),
108
- report_uri_directive,
109
- script_nonce_directive,
110
- ].join
111
-
112
- #store the value for next time
113
- @config = header_value
114
- header_value.strip
115
- rescue StandardError => e
116
- raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
171
+ generic_directives,
172
+ non_default_directives,
173
+ report_uri_directive
174
+ ].join.strip
117
175
  end
118
176
 
119
177
  def fill_directives
120
- return unless @config[:default_src]
121
-
122
- default = @config[:default_src]
123
- directives.each do |directive|
124
- unless @config[directive]
125
- @config[directive] = default
178
+ if default = @config[:default_src]
179
+ DIRECTIVES.each do |directive|
180
+ unless @config[directive]
181
+ @config[directive] = default
182
+ end
126
183
  end
127
184
  end
128
- @config
129
185
  end
130
186
 
131
187
  def append_http_additions
132
- return unless http_additions
133
-
134
- http_additions.each do |k, v|
188
+ return unless @http_additions
189
+ @http_additions.each do |k, v|
135
190
  @config[k] ||= []
136
191
  @config[k] << v
137
192
  end
138
193
  end
139
194
 
140
- def normalize_csp_options
141
- @config.each do |k,v|
142
- @config[k] = v.split if v.is_a? String
143
- @config[k] = @config[k].map do |val|
144
- translate_dir_value(val)
145
- end
146
- end
147
- end
148
-
149
- def filter_unsupported_directives
150
- @config = browser_strategy.filter_unsupported_directives(@config)
151
- end
152
-
153
- # translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
154
195
  def translate_dir_value val
155
196
  if %w{inline eval}.include?(val)
156
- translate_inline_or_eval(val)
157
- # self/none are special sources/src-dir-values and need to be quoted in chrome
197
+ val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
198
+ # self/none are special sources/src-dir-values and need to be quoted
158
199
  elsif %{self none}.include?(val)
159
200
  "'#{val}'"
201
+ elsif val == 'nonce'
202
+ self.class.set_nonce(@controller, nonce)
203
+ ["'nonce-#{nonce}'", "'unsafe-inline'"]
160
204
  else
161
205
  val
162
206
  end
163
207
  end
164
208
 
165
- def translate_inline_or_eval val
166
- browser_strategy.translate_inline_or_eval(val)
167
- end
168
-
169
- # if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
170
- # or only a path was supplied (in which case we assume cross-host)
171
- # we need to forward the request for Firefox.
172
- def normalize_reporting_endpoint
173
- return unless browser_strategy.normalize_reporting_endpoint?
174
- if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
175
- return
176
- end
177
-
178
- if forward_endpoint
179
- @report_uri = FF_CSP_ENDPOINT
180
- end
181
- end
182
-
183
- def build_impl_specific_directives
184
- default = expect_directive_value(:default_src)
185
- browser_strategy.build_impl_specific_directives(default)
186
- end
187
-
188
- def expect_directive_value key
189
- @config.delete(key) {|k| raise ContentSecurityPolicyBuildError.new("Expected to find #{k} directive value")}
190
- end
191
-
192
- def same_origin?
193
- return unless report_uri && request_uri
194
-
195
- origin = URI.parse(request_uri)
196
- uri = URI.parse(report_uri)
197
- uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
198
- end
199
-
200
209
  def report_uri_directive
201
210
  return '' if @report_uri.nil?
202
211
 
@@ -208,50 +217,40 @@ module SecureHeaders
208
217
  end
209
218
  end
210
219
 
211
- "report-uri #{@report_uri};"
212
- 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
213
224
 
214
- def script_nonce_directive
215
- return '' if @script_nonce.nil?
216
- nonce_value = if @script_nonce.is_a?(String)
217
- @script_nonce
218
- elsif @controller
219
- @controller.instance_exec(&@script_nonce)
220
- else
221
- @script_nonce.call
222
- end
223
- "script-nonce #{nonce_value};"
225
+ "report-uri #{@report_uri};"
224
226
  end
225
227
 
226
- def generic_directives(config)
228
+ def generic_directives
227
229
  header_value = ''
228
- if config[:img_src]
229
- 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:')
230
232
  else
231
- config[:img_src] = ['data:']
233
+ @config[:img_src] = @config[:default_src] + ['data:']
232
234
  end
233
235
 
234
- config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
235
- header_value += "#{symbol_to_hyphen_case(k)} #{config[k].join(" ")}; "
236
+ DIRECTIVES.each do |directive_name|
237
+ header_value += build_directive(directive_name) if @config[directive_name]
236
238
  end
237
239
 
238
240
  header_value
239
241
  end
240
242
 
241
- def symbol_to_hyphen_case sym
242
- sym.to_s.gsub('_', '-')
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
248
+
249
+ header_value
243
250
  end
244
251
 
245
- def parse_request request
246
- @browser = Brwsr::Browser.new(:ua => request.env['HTTP_USER_AGENT'])
247
- @ssl_request = request.ssl?
248
- @request_uri = if request.respond_to?(:original_url)
249
- # rails 3.1+
250
- request.original_url
251
- else
252
- # rails 2/3.0
253
- request.url
254
- end
252
+ def build_directive(key)
253
+ "#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
255
254
  end
256
255
  end
257
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.0.0"
2
+ VERSION = "2.0.0"
3
3
  end