secure_headers 1.1.1 → 2.1.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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -8
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +8 -3
  6. data/Gemfile +6 -11
  7. data/README.md +219 -104
  8. data/Rakefile +11 -116
  9. data/fixtures/rails_3_2_12/Gemfile +0 -8
  10. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
  11. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
  12. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  13. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
  14. data/fixtures/rails_3_2_12/config/application.rb +0 -54
  15. data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
  16. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +3 -1
  17. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  18. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  19. data/fixtures/rails_3_2_12/config.ru +3 -0
  20. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +57 -19
  21. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +18 -13
  22. data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
  23. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -9
  24. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  25. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  26. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  27. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  28. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
  29. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
  30. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  31. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +17 -12
  32. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +17 -12
  33. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
  34. data/fixtures/rails_4_1_8/Gemfile +5 -0
  35. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  36. data/fixtures/rails_4_1_8/Rakefile +6 -0
  37. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  38. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  39. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  40. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  41. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  42. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  43. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  44. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  45. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  46. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  47. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  48. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  49. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  50. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  51. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  52. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  53. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  54. data/fixtures/rails_4_1_8/config.ru +4 -0
  55. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  56. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  57. data/fixtures/rails_4_1_8/log/.keep +0 -0
  58. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  59. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  60. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  61. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  62. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  63. data/lib/secure_headers/hash_helper.rb +7 -0
  64. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  65. data/lib/secure_headers/headers/content_security_policy.rb +160 -129
  66. data/lib/secure_headers/headers/public_key_pins.rb +95 -0
  67. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  68. data/lib/secure_headers/headers/x_download_options.rb +39 -0
  69. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  70. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  71. data/lib/secure_headers/padrino.rb +14 -0
  72. data/lib/secure_headers/railtie.rb +19 -24
  73. data/lib/secure_headers/version.rb +1 -1
  74. data/lib/secure_headers/view_helper.rb +68 -0
  75. data/lib/secure_headers.rb +78 -16
  76. data/lib/tasks/tasks.rake +48 -0
  77. data/secure_headers.gemspec +4 -4
  78. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  79. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +127 -220
  80. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +37 -0
  81. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +21 -20
  82. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
  83. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
  84. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
  85. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  86. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -19
  87. data/spec/lib/secure_headers_spec.rb +115 -73
  88. data/spec/spec_helper.rb +30 -18
  89. metadata +62 -58
  90. data/.rvmrc +0 -1
  91. data/Guardfile +0 -10
  92. data/HISTORY.md +0 -115
  93. data/app/controllers/content_security_policy_controller.rb +0 -75
  94. data/config/curl-ca-bundle.crt +0 -5420
  95. data/config/routes.rb +0 -3
  96. data/fixtures/rails_3_2_12/Guardfile +0 -14
  97. data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
  98. data/fixtures/rails_3_2_12/config/database.yml +0 -25
  99. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  100. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  101. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  102. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  103. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  104. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  105. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  106. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  107. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  108. data/fixtures/rails_3_2_12/db/schema.rb +0 -16
  109. data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
  110. data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
  111. data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
  112. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  113. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  114. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  115. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  116. data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
  117. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  118. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  119. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  120. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  121. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  122. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  123. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  124. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  125. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  126. data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
  127. data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
  128. data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -3,7 +3,24 @@ if defined?(Rails::Railtie)
3
3
  module SecureHeaders
4
4
  class Railtie < Rails::Engine
5
5
  isolate_namespace ::SecureHeaders if defined? isolate_namespace # rails 3.0
6
- ActionController::Base.send :include, ::SecureHeaders
6
+ conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Content-Type-Options',
7
+ 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
+ 'X-Content-Type-Options', 'Strict-Transport-Security',
9
+ 'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
+ 'X-Permitted-Cross-Domain-Policies','Public-Key-Pins','Public-Key-Pins-Report-Only']
11
+
12
+ initializer "secure_headers.action_controller" do
13
+ ActiveSupport.on_load(:action_controller) do
14
+ include ::SecureHeaders
15
+
16
+ unless Rails.application.config.action_dispatch.default_headers.nil?
17
+ conflicting_headers.each do |header|
18
+ Rails.application.config.action_dispatch.default_headers.delete(header)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
7
24
  end
8
25
  end
9
26
  else
@@ -12,26 +29,4 @@ else
12
29
  include ::SecureHeaders
13
30
  end
14
31
  end
15
-
16
- module SecureHeaders
17
- module Routing
18
- module MapperExtensions
19
- def csp_endpoint
20
- @set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
21
- end
22
- end
23
- end
24
- end
25
-
26
- if defined?(ActiveSupport::Dependencies)
27
- if ActiveSupport::Dependencies.autoload_paths
28
- ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
29
- else
30
- ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
31
- end
32
- end
33
-
34
- if defined? ActionController::Routing
35
- ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
36
- end
37
- end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "1.1.1"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -0,0 +1,68 @@
1
+ module SecureHeaders
2
+ class UnexpectedHashedScriptException < StandardError
3
+
4
+ end
5
+
6
+ module ViewHelpers
7
+ include SecureHeaders::HashHelper
8
+ SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
9
+
10
+ def nonced_style_tag(content = nil, &block)
11
+ nonced_tag(content, :style, block)
12
+ end
13
+
14
+ def nonced_javascript_tag(content = nil, &block)
15
+ nonced_tag(content, :script, block)
16
+ end
17
+
18
+ def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block)
19
+ content = capture(&block)
20
+
21
+ if ['development', 'test'].include?(ENV["RAILS_ENV"])
22
+ hash_value = hash_source(content)
23
+ file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
24
+ script_hashes = controller.instance_variable_get(:@script_hashes)[file_path]
25
+ unless script_hashes && script_hashes.include?(hash_value)
26
+ message = unexpected_hash_error_message(file_path, hash_value, content)
27
+ if raise_error_on_unrecognized_hash
28
+ raise UnexpectedHashedScriptException.new(message)
29
+ else
30
+ puts message
31
+ request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
32
+ end
33
+ end
34
+ end
35
+
36
+ content_tag :script, content
37
+ end
38
+
39
+ private
40
+
41
+ def nonced_tag(content, type, block)
42
+ content = if block
43
+ capture(&block)
44
+ else
45
+ content.html_safe # :'(
46
+ end
47
+
48
+ content_tag type, content, :nonce => @content_security_policy_nonce
49
+ end
50
+
51
+ def unexpected_hash_error_message(file_path, hash_value, content)
52
+ <<-EOF
53
+ \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
54
+ <script>#{content}</script>
55
+ *** This is fine in dev/test, but will raise exceptions in production. ***
56
+ *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
57
+ #{file_path}:
58
+ - #{hash_value}\n\n
59
+ EOF
60
+ end
61
+ end
62
+ end
63
+
64
+ module ActionView #:nodoc:
65
+ class Base #: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
8
+ :x_xss_protection, :csp, :x_download_options, :script_hashes,
9
+ :x_permitted_cross_domain_policies, :hpkp
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,15 @@ 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
45
+ before_filter :set_hpkp_header
37
46
  before_filter :set_x_frame_options_header
38
47
  before_filter :set_csp_header
39
48
  before_filter :set_x_xss_protection_header
40
49
  before_filter :set_x_content_type_options_header
50
+ before_filter :set_x_download_options_header
51
+ before_filter :set_x_permitted_cross_domain_policies_header
41
52
  end
42
53
 
43
54
  # we can't use ||= because I'm overloading false => disable, nil => default
@@ -48,37 +59,65 @@ module SecureHeaders
48
59
  end
49
60
 
50
61
  module InstanceMethods
51
- # Re-added for backwards compat.
52
62
  def set_security_headers(options = self.class.secure_headers_options)
53
63
  set_csp_header(request, options[:csp])
54
64
  set_hsts_header(options[:hsts])
65
+ set_hpkp_header(options[:hpkp])
55
66
  set_x_frame_options_header(options[:x_frame_options])
56
67
  set_x_xss_protection_header(options[:x_xss_protection])
57
68
  set_x_content_type_options_header(options[:x_content_type_options])
69
+ set_x_download_options_header(options[:x_download_options])
70
+ set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
58
71
  end
59
72
 
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
73
  # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
63
74
  # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
64
75
  # set_csp_header(+Hash+) - uses the request accessor and options from parameters
65
76
  # 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
77
+ def set_csp_header(req = nil, config=nil)
78
+ if req.is_a?(Hash) || req.is_a?(FalseClass)
79
+ config = req
70
80
  end
71
81
 
72
- options = self.class.secure_headers_options[:csp] if options.nil?
73
- options = self.class.options_for :csp, options
82
+ config = self.class.secure_headers_options[:csp] if config.nil?
83
+ config = self.class.options_for :csp, config
74
84
 
75
- return if options == false
85
+ return if config == false
86
+
87
+ if config && config[:script_hash_middleware]
88
+ ContentSecurityPolicy.add_to_env(request, self, config)
89
+ else
90
+ csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
91
+ set_header(csp_header)
92
+ end
93
+ end
94
+
95
+
96
+ def prep_script_hash
97
+ if ::SecureHeaders::Configuration.script_hashes
98
+ @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
99
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
100
+ save_hash_for_later payload
101
+ end
102
+
103
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
104
+ save_hash_for_later payload
105
+ end
106
+ end
107
+ end
108
+
109
+ def save_hash_for_later payload
110
+ matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
76
111
 
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)
112
+ if payload[:layout]
113
+ # 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
114
+ layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
115
+
116
+ matching_hashes << layout_hashes if layout_hashes
117
+ end
118
+
119
+ if matching_hashes.any?
120
+ request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
82
121
  end
83
122
  end
84
123
 
@@ -99,6 +138,24 @@ module SecureHeaders
99
138
  set_a_header(:hsts, StrictTransportSecurity, options)
100
139
  end
101
140
 
141
+ def set_hpkp_header(options=self.class.secure_headers_options[:hpkp])
142
+ return unless request.ssl?
143
+ config = self.class.options_for :hpkp, options
144
+
145
+ return if config == false || config.nil?
146
+
147
+ hpkp_header = PublicKeyPins.new(config)
148
+ set_header(hpkp_header)
149
+ end
150
+
151
+ def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
152
+ set_a_header(:x_download_options, XDownloadOptions, options)
153
+ end
154
+
155
+ def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
156
+ set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
157
+ end
158
+
102
159
  private
103
160
 
104
161
  def set_a_header(name, klass, options=nil)
@@ -123,9 +180,14 @@ end
123
180
 
124
181
  require "secure_headers/version"
125
182
  require "secure_headers/header"
183
+ require "secure_headers/headers/public_key_pins"
126
184
  require "secure_headers/headers/content_security_policy"
127
185
  require "secure_headers/headers/x_frame_options"
128
186
  require "secure_headers/headers/strict_transport_security"
129
187
  require "secure_headers/headers/x_xss_protection"
130
188
  require "secure_headers/headers/x_content_type_options"
189
+ require "secure_headers/headers/x_download_options"
190
+ require "secure_headers/headers/x_permitted_cross_domain_policies"
131
191
  require "secure_headers/railtie"
192
+ require "secure_headers/hash_helper"
193
+ 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