secure_headers 1.3.4 → 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 (100) hide show
  1. data/.gitignore +4 -8
  2. data/.travis.yml +7 -4
  3. data/Gemfile +6 -7
  4. data/README.md +115 -79
  5. data/Rakefile +11 -116
  6. data/fixtures/rails_3_2_12/Gemfile +0 -5
  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/config/application.rb +0 -54
  11. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +2 -1
  12. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  13. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  14. data/fixtures/rails_3_2_12/config.ru +3 -0
  15. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +55 -18
  16. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +6 -1
  17. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -6
  18. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  19. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  20. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  21. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  22. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -48
  23. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  24. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +5 -0
  25. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +5 -0
  26. data/fixtures/rails_4_1_8/Gemfile +5 -0
  27. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  28. data/fixtures/rails_4_1_8/Rakefile +6 -0
  29. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  30. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  31. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  32. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  33. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  34. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  35. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  36. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  37. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  38. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  39. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  40. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  41. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  42. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  43. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  44. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  45. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  46. data/fixtures/rails_4_1_8/config.ru +4 -0
  47. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  48. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  49. data/fixtures/rails_4_1_8/log/.keep +0 -0
  50. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  51. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  52. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  53. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  54. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  55. data/lib/secure_headers/hash_helper.rb +7 -0
  56. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  57. data/lib/secure_headers/headers/content_security_policy.rb +141 -133
  58. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  59. data/lib/secure_headers/railtie.rb +13 -22
  60. data/lib/secure_headers/version.rb +1 -1
  61. data/lib/secure_headers/view_helper.rb +68 -0
  62. data/lib/secure_headers.rb +58 -16
  63. data/lib/tasks/tasks.rake +48 -0
  64. data/secure_headers.gemspec +4 -4
  65. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  66. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
  67. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +1 -1
  68. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  69. data/spec/lib/secure_headers_spec.rb +36 -61
  70. data/spec/spec_helper.rb +26 -1
  71. metadata +51 -37
  72. checksums.yaml +0 -15
  73. data/Guardfile +0 -6
  74. data/HISTORY.md +0 -162
  75. data/app/controllers/content_security_policy_controller.rb +0 -75
  76. data/config/curl-ca-bundle.crt +0 -5420
  77. data/config/routes.rb +0 -3
  78. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  79. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  80. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  81. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  82. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  83. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  84. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  85. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  86. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  87. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  88. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  89. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  90. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  91. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  92. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  93. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  94. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  95. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  96. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  97. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  98. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  99. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  100. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -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
@@ -1,11 +1,18 @@
1
1
  module SecureHeaders
2
+ SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
3
+ HASHES_ENV_KEY = 'secure_headers.script_hashes'
4
+
2
5
  module Configuration
3
6
  class << self
4
7
  attr_accessor :hsts, :x_frame_options, :x_content_type_options,
5
- :x_xss_protection, :csp, :x_download_options
8
+ :x_xss_protection, :csp, :x_download_options, :script_hashes,
9
+ :x_permitted_cross_domain_policies
6
10
 
7
11
  def configure &block
8
12
  instance_eval &block
13
+ if File.exists?(SCRIPT_HASH_CONFIG_FILE)
14
+ ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
15
+ end
9
16
  end
10
17
  end
11
18
  end
@@ -33,12 +40,14 @@ module SecureHeaders
33
40
 
34
41
  def ensure_security_headers options = {}
35
42
  self.secure_headers_options = options
43
+ before_filter :prep_script_hash
36
44
  before_filter :set_hsts_header
37
45
  before_filter :set_x_frame_options_header
38
46
  before_filter :set_csp_header
39
47
  before_filter :set_x_xss_protection_header
40
48
  before_filter :set_x_content_type_options_header
41
49
  before_filter :set_x_download_options_header
50
+ before_filter :set_x_permitted_cross_domain_policies_header
42
51
  end
43
52
 
44
53
  # we can't use ||= because I'm overloading false => disable, nil => default
@@ -49,7 +58,6 @@ module SecureHeaders
49
58
  end
50
59
 
51
60
  module InstanceMethods
52
- # Re-added for backwards compat.
53
61
  def set_security_headers(options = self.class.secure_headers_options)
54
62
  set_csp_header(request, options[:csp])
55
63
  set_hsts_header(options[:hsts])
@@ -57,30 +65,57 @@ module SecureHeaders
57
65
  set_x_xss_protection_header(options[:x_xss_protection])
58
66
  set_x_content_type_options_header(options[:x_content_type_options])
59
67
  set_x_download_options_header(options[:x_download_options])
68
+ set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
60
69
  end
61
70
 
62
- # backwards compatibility jank, to be removed in 1.0. Old API required a request
63
- # object when it didn't really need to.
64
71
  # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
65
72
  # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
66
73
  # set_csp_header(+Hash+) - uses the request accessor and options from parameters
67
74
  # set_csp_header(+Rack::Request+, +Hash+)
68
- def set_csp_header(req = nil, options=nil)
69
- # hack to help generating headers statically
70
- if req.is_a?(Hash)
71
- options = req
75
+ def set_csp_header(req = nil, config=nil)
76
+ if req.is_a?(Hash) || req.is_a?(FalseClass)
77
+ config = req
72
78
  end
73
79
 
74
- options = self.class.secure_headers_options[:csp] if options.nil?
75
- options = self.class.options_for :csp, options
80
+ config = self.class.secure_headers_options[:csp] if config.nil?
81
+ config = self.class.options_for :csp, config
76
82
 
77
- return if options == false
83
+ return if config == false
78
84
 
79
- csp_header = ContentSecurityPolicy.new(options, :request => request, :controller => self)
80
- set_header(csp_header)
81
- if options && options[:experimental] && options[:enforce]
82
- experimental_header = ContentSecurityPolicy.new(options, :experimental => true, :request => request, :controller => self)
83
- set_header(experimental_header)
85
+ if config && config[:script_hash_middleware]
86
+ ContentSecurityPolicy.add_to_env(request, self, config)
87
+ else
88
+ csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
89
+ set_header(csp_header)
90
+ end
91
+ end
92
+
93
+
94
+ def prep_script_hash
95
+ if ::SecureHeaders::Configuration.script_hashes
96
+ @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
97
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
98
+ save_hash_for_later payload
99
+ end
100
+
101
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
102
+ save_hash_for_later payload
103
+ end
104
+ end
105
+ end
106
+
107
+ def save_hash_for_later payload
108
+ matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
109
+
110
+ if payload[:layout]
111
+ # We're assuming an html.erb layout for now. Will need to handle mustache too, just not sure of the best way to do this
112
+ layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
113
+
114
+ matching_hashes << layout_hashes if layout_hashes
115
+ end
116
+
117
+ if matching_hashes.any?
118
+ request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
84
119
  end
85
120
  end
86
121
 
@@ -105,6 +140,10 @@ module SecureHeaders
105
140
  set_a_header(:x_download_options, XDownloadOptions, options)
106
141
  end
107
142
 
143
+ def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
144
+ set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
145
+ end
146
+
108
147
  private
109
148
 
110
149
  def set_a_header(name, klass, options=nil)
@@ -135,4 +174,7 @@ require "secure_headers/headers/strict_transport_security"
135
174
  require "secure_headers/headers/x_xss_protection"
136
175
  require "secure_headers/headers/x_content_type_options"
137
176
  require "secure_headers/headers/x_download_options"
177
+ require "secure_headers/headers/x_permitted_cross_domain_policies"
138
178
  require "secure_headers/railtie"
179
+ require "secure_headers/hash_helper"
180
+ require "secure_headers/view_helper"
@@ -0,0 +1,48 @@
1
+ INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
2
+ INLINE_HASH_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
3
+ SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
4
+
5
+ namespace :secure_headers do
6
+ include SecureHeaders::HashHelper
7
+
8
+ def is_erb?(filename)
9
+ filename =~ /\.erb\Z/
10
+ end
11
+
12
+ def generate_inline_script_hashes(filename)
13
+ file = File.read(filename)
14
+ hashes = []
15
+
16
+ [INLINE_SCRIPT_REGEX, INLINE_HASH_HELPER_REGEX].each do |regex|
17
+ file.gsub(regex) do # TODO don't use gsub
18
+ inline_script = Regexp.last_match.captures.last
19
+ if (filename =~ /\.mustache\Z/ && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/)
20
+ puts "Looks like there's some dynamic content inside of a script tag :-/"
21
+ puts "That pretty much means the hash value will never match."
22
+ puts "Code: " + inline_script
23
+ puts "=" * 20
24
+ end
25
+
26
+ hashes << hash_source(inline_script)
27
+ end
28
+ end
29
+
30
+ hashes
31
+ end
32
+
33
+ task :generate_hashes do |t, args|
34
+ script_hashes = {}
35
+ Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename|
36
+ hashes = generate_inline_script_hashes(filename)
37
+ if hashes.any?
38
+ script_hashes[filename] = hashes
39
+ end
40
+ end
41
+
42
+ File.open(SCRIPT_HASH_CONFIG_FILE, 'w') do |file|
43
+ file.write(script_hashes.to_yaml)
44
+ end
45
+
46
+ puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SCRIPT_HASH_CONFIG_FILE}"
47
+ end
48
+ end
@@ -8,10 +8,10 @@ Gem::Specification.new do |gem|
8
8
  gem.version = SecureHeaders::VERSION
9
9
  gem.authors = ["Neil Matatall"]
10
10
  gem.email = ["neil.matatall@gmail.com"]
11
- gem.description = %q{Add easily configured browser headers to responses.}
12
- gem.summary = %q{Add easily configured browser headers to responses
13
- including content security policy, x-frame-options,
14
- strict-transport-security and more.}
11
+ gem.description = %q{Security related headers all in one gem.}
12
+ gem.summary = %q{Add easily configured security headers to responses
13
+ including content-security-policy, x-frame-options,
14
+ strict-transport-security, etc.}
15
15
  gem.homepage = "https://github.com/twitter/secureheaders"
16
16
  gem.license = "Apache Public License 2.0"
17
17
  gem.files = `git ls-files`.split($/)
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'secure_headers/headers/content_security_policy/script_hash_middleware'
3
+
4
+ module SecureHeaders
5
+ describe ContentSecurityPolicy::ScriptHashMiddleware do
6
+
7
+ let(:app) { double(:call => [200, headers, '']) }
8
+ let(:env) { double }
9
+ let(:headers) { double }
10
+
11
+ let(:default_config) do
12
+ {
13
+ :disable_fill_missing => true,
14
+ :default_src => 'https://*',
15
+ :report_uri => '/csp_report',
16
+ :script_src => 'inline eval https://* data:',
17
+ :style_src => "inline https://* about:"
18
+ }
19
+ end
20
+
21
+ def should_assign_header name, value
22
+ expect(headers).to receive(:[]=).with(name, value)
23
+ end
24
+
25
+ def call_middleware(hashes = [])
26
+ options = {
27
+ :ua => USER_AGENTS[:chrome]
28
+ }
29
+ expect(env).to receive(:[]).with(HASHES_ENV_KEY).and_return(hashes)
30
+ expect(env).to receive(:[]).with(ENV_KEY).and_return(
31
+ :config => default_config,
32
+ :options => options
33
+ )
34
+ ContentSecurityPolicy::ScriptHashMiddleware.new(app).call(env)
35
+ end
36
+
37
+ it "adds hashes stored in env to the header" do
38
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*'sha256-/)
39
+ call_middleware(['sha256-abc123'])
40
+ end
41
+
42
+ it "leaves things alone when no hashes are saved to env" do
43
+ should_assign_header(HEADER_NAME + "-Report-Only", /script-src[^;]*(?!'sha256-)/)
44
+ call_middleware()
45
+ end
46
+ end
47
+ end