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.
- checksums.yaml +7 -0
- data/.gitignore +4 -8
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -3
- data/Gemfile +6 -11
- data/README.md +219 -104
- data/Rakefile +11 -116
- data/fixtures/rails_3_2_12/Gemfile +0 -8
- data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
- data/fixtures/rails_3_2_12/config/application.rb +0 -54
- data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +3 -1
- data/fixtures/rails_3_2_12/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
- data/fixtures/rails_3_2_12/config.ru +3 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +57 -19
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +18 -13
- data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -9
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
- data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
- data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
- data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
- data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +17 -12
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +17 -12
- data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
- data/fixtures/rails_4_1_8/Gemfile +5 -0
- data/fixtures/rails_4_1_8/README.rdoc +28 -0
- data/fixtures/rails_4_1_8/Rakefile +6 -0
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
- data/fixtures/rails_4_1_8/config/application.rb +15 -0
- data/fixtures/rails_4_1_8/config/boot.rb +4 -0
- data/fixtures/rails_4_1_8/config/environment.rb +5 -0
- data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
- data/fixtures/rails_4_1_8/config/routes.rb +4 -0
- data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
- data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
- data/fixtures/rails_4_1_8/config.ru +4 -0
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/hash_helper.rb +7 -0
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
- data/lib/secure_headers/headers/content_security_policy.rb +160 -129
- data/lib/secure_headers/headers/public_key_pins.rb +95 -0
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +39 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/padrino.rb +14 -0
- data/lib/secure_headers/railtie.rb +19 -24
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +78 -16
- data/lib/tasks/tasks.rake +48 -0
- data/secure_headers.gemspec +4 -4
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +127 -220
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +37 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +21 -20
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -19
- data/spec/lib/secure_headers_spec.rb +115 -73
- data/spec/spec_helper.rb +30 -18
- metadata +62 -58
- data/.rvmrc +0 -1
- data/Guardfile +0 -10
- data/HISTORY.md +0 -115
- data/app/controllers/content_security_policy_controller.rb +0 -75
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- data/fixtures/rails_3_2_12/Guardfile +0 -14
- data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12/config/database.yml +0 -25
- data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
- data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
- data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
- data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
- data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
- 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
|
-
|
|
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
|
|
@@ -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
|
data/lib/secure_headers.rb
CHANGED
|
@@ -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,
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
82
|
+
config = self.class.secure_headers_options[:csp] if config.nil?
|
|
83
|
+
config = self.class.options_for :csp, config
|
|
74
84
|
|
|
75
|
-
return if
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
data/secure_headers.gemspec
CHANGED
|
@@ -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{
|
|
12
|
-
gem.summary = %q{Add easily configured
|
|
13
|
-
including content
|
|
14
|
-
strict-transport-security
|
|
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
|