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,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
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,11 +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
49
+ before_filter :set_x_download_options_header
50
+ before_filter :set_x_permitted_cross_domain_policies_header
41
51
  end
42
52
 
43
53
  # we can't use ||= because I'm overloading false => disable, nil => default
@@ -48,37 +58,64 @@ module SecureHeaders
48
58
  end
49
59
 
50
60
  module InstanceMethods
51
- # Re-added for backwards compat.
52
61
  def set_security_headers(options = self.class.secure_headers_options)
53
62
  set_csp_header(request, options[:csp])
54
63
  set_hsts_header(options[:hsts])
55
64
  set_x_frame_options_header(options[:x_frame_options])
56
65
  set_x_xss_protection_header(options[:x_xss_protection])
57
66
  set_x_content_type_options_header(options[:x_content_type_options])
67
+ set_x_download_options_header(options[:x_download_options])
68
+ set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
58
69
  end
59
70
 
60
- # backwards compatibility jank, to be removed in 1.0. Old API required a request
61
- # object when it didn't really need to.
62
71
  # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
63
72
  # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
64
73
  # set_csp_header(+Hash+) - uses the request accessor and options from parameters
65
74
  # set_csp_header(+Rack::Request+, +Hash+)
66
- def set_csp_header(req = nil, options=nil)
67
- # hack to help generating headers statically
68
- if req.is_a?(Hash)
69
- options = req
75
+ def set_csp_header(req = nil, config=nil)
76
+ if req.is_a?(Hash) || req.is_a?(FalseClass)
77
+ config = req
70
78
  end
71
79
 
72
- options = self.class.secure_headers_options[:csp] if options.nil?
73
- 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
74
82
 
75
- return if options == false
83
+ return if config == false
84
+
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
76
100
 
77
- csp_header = ContentSecurityPolicy.new(options, :request => request, :controller => self)
78
- set_header(csp_header)
79
- if options && options[:experimental] && options[:enforce]
80
- experimental_header = ContentSecurityPolicy.new(options, :experimental => true, :request => request, :controller => self)
81
- set_header(experimental_header)
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
82
119
  end
83
120
  end
84
121
 
@@ -99,6 +136,14 @@ module SecureHeaders
99
136
  set_a_header(:hsts, StrictTransportSecurity, options)
100
137
  end
101
138
 
139
+ def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
140
+ set_a_header(:x_download_options, XDownloadOptions, options)
141
+ end
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
+
102
147
  private
103
148
 
104
149
  def set_a_header(name, klass, options=nil)
@@ -128,4 +173,8 @@ require "secure_headers/headers/x_frame_options"
128
173
  require "secure_headers/headers/strict_transport_security"
129
174
  require "secure_headers/headers/x_xss_protection"
130
175
  require "secure_headers/headers/x_content_type_options"
176
+ require "secure_headers/headers/x_download_options"
177
+ require "secure_headers/headers/x_permitted_cross_domain_policies"
131
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