secure_headers 1.1.1 → 2.0.2
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.
- 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 +185 -102
- 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/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 +65 -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/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 +68 -73
- data/spec/spec_helper.rb +29 -18
- metadata +51 -46
- 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
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
|
|
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,
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
80
|
+
config = self.class.secure_headers_options[:csp] if config.nil?
|
|
81
|
+
config = self.class.options_for :csp, config
|
|
74
82
|
|
|
75
|
-
return if
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
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
|