secure_headers 1.4.1 → 2.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.gitignore +4 -8
- data/Gemfile +2 -2
- data/Guardfile +8 -0
- data/README.md +102 -48
- data/Rakefile +0 -116
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -1
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -1
- 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 +50 -18
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
- 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 +141 -137
- data/lib/secure_headers/railtie.rb +0 -22
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +51 -17
- data/lib/tasks/tasks.rake +48 -0
- 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 +83 -208
- data/spec/lib/secure_headers_spec.rb +16 -62
- data/spec/spec_helper.rb +25 -1
- metadata +22 -24
- data/HISTORY.md +0 -162
- data/app/controllers/content_security_policy_controller.rb +0 -76
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
@@ -1,93 +1,154 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'base64'
|
3
|
+
require 'securerandom'
|
3
4
|
|
4
5
|
module SecureHeaders
|
5
6
|
class ContentSecurityPolicyBuildError < StandardError; end
|
6
7
|
class ContentSecurityPolicy < Header
|
7
8
|
module Constants
|
8
|
-
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https
|
9
|
-
|
10
|
-
|
11
|
-
DIRECTIVES = [
|
12
|
-
|
9
|
+
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
|
10
|
+
HEADER_NAME = "Content-Security-Policy"
|
11
|
+
ENV_KEY = 'secure_headers.content_security_policy'
|
12
|
+
DIRECTIVES = [
|
13
|
+
:default_src,
|
14
|
+
:connect_src,
|
15
|
+
:font_src,
|
16
|
+
:frame_src,
|
17
|
+
:img_src,
|
18
|
+
:media_src,
|
19
|
+
:object_src,
|
20
|
+
:script_src,
|
21
|
+
:style_src
|
22
|
+
]
|
23
|
+
|
24
|
+
NON_DEFAULT_SOURCES = [
|
25
|
+
:base_uri,
|
26
|
+
:child_src,
|
27
|
+
:form_action,
|
28
|
+
:frame_ancestors,
|
29
|
+
:plugin_types,
|
30
|
+
:referrer,
|
31
|
+
:reflected_xss
|
32
|
+
]
|
33
|
+
|
34
|
+
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
|
13
35
|
end
|
14
36
|
include Constants
|
15
37
|
|
16
|
-
|
17
|
-
attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
|
18
|
-
|
19
|
-
alias :disable_chrome_extension? :disable_chrome_extension
|
38
|
+
attr_reader :disable_fill_missing, :ssl_request
|
20
39
|
alias :disable_fill_missing? :disable_fill_missing
|
21
40
|
alias :ssl_request? :ssl_request
|
22
41
|
|
42
|
+
class << self
|
43
|
+
def generate_nonce
|
44
|
+
SecureRandom.base64(32).chomp
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_nonce(controller, nonce = generate_nonce)
|
48
|
+
controller.instance_variable_set(:@content_security_policy_nonce, nonce)
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_to_env(request, controller, config)
|
52
|
+
set_nonce(controller)
|
53
|
+
options = options_from_request(request).merge(:controller => controller)
|
54
|
+
request.env[Constants::ENV_KEY] = {
|
55
|
+
:config => config,
|
56
|
+
:options => options,
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def options_from_request(request)
|
61
|
+
{
|
62
|
+
:ssl => request.ssl?,
|
63
|
+
:ua => request.env['HTTP_USER_AGENT'],
|
64
|
+
:request_uri => request_uri_from_request(request),
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def request_uri_from_request(request)
|
69
|
+
if request.respond_to?(:original_url)
|
70
|
+
# rails 3.1+
|
71
|
+
request.original_url
|
72
|
+
else
|
73
|
+
# rails 2/3.0
|
74
|
+
request.url
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def symbol_to_hyphen_case sym
|
79
|
+
sym.to_s.gsub('_', '-')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
23
83
|
# +options+ param contains
|
24
|
-
# :
|
84
|
+
# :controller used for setting instance variables for nonces/hashes
|
25
85
|
# :ssl_request used to determine if http_additions should be used
|
26
|
-
# :request_uri used to determine if firefox should send the report directly
|
27
|
-
# or use the forwarding endpoint
|
28
86
|
# :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
|
29
87
|
#
|
30
88
|
# :report used to determine what :ssl_request, :ua, and :request_uri are set to
|
31
89
|
def initialize(config=nil, options={})
|
32
|
-
|
33
|
-
if @experimental
|
34
|
-
warn "[DEPRECATION] 'experimental' config is removed in 2.0"
|
35
|
-
end
|
36
|
-
@controller = options.delete(:controller)
|
90
|
+
return unless config
|
37
91
|
|
38
92
|
if options[:request]
|
39
|
-
|
40
|
-
else
|
41
|
-
@ua = options[:ua]
|
42
|
-
# fails open, assumes http. Bad idea? Will always include http additions.
|
43
|
-
# could also fail if not supplied.
|
44
|
-
@ssl_request = !!options.delete(:ssl)
|
45
|
-
# a nil value here means we always assume we are not on the same host,
|
46
|
-
# which causes all FF csp reports to go through the forwarder
|
47
|
-
@request_uri = options.delete(:request_uri)
|
93
|
+
options = options.merge(self.class.options_from_request(options[:request]))
|
48
94
|
end
|
49
95
|
|
50
|
-
|
51
|
-
|
96
|
+
@controller = options[:controller]
|
97
|
+
@ua = options[:ua]
|
98
|
+
@ssl_request = !!options.delete(:ssl)
|
99
|
+
@request_uri = options.delete(:request_uri)
|
52
100
|
|
53
|
-
|
54
|
-
@
|
55
|
-
|
101
|
+
# Config values can be string, array, or lamdba values
|
102
|
+
@config = config.inject({}) do |hash, (key, value)|
|
103
|
+
config_val = value.respond_to?(:call) ? value.call : value
|
56
104
|
|
57
|
-
|
58
|
-
|
105
|
+
if ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
|
106
|
+
config_val = config_val.split if config_val.is_a? String
|
107
|
+
if config_val.is_a?(Array)
|
108
|
+
config_val = config_val.map do |val|
|
109
|
+
translate_dir_value(val)
|
110
|
+
end.flatten.uniq
|
111
|
+
end
|
112
|
+
end
|
59
113
|
|
60
|
-
|
61
|
-
|
62
|
-
@config[:http_additions] = experimental_config[:http_additions]
|
63
|
-
@config.merge!(experimental_config)
|
114
|
+
hash[key] = config_val
|
115
|
+
hash
|
64
116
|
end
|
65
117
|
|
66
|
-
# these values don't support lambdas because this needs to be rewritten
|
67
118
|
@http_additions = @config.delete(:http_additions)
|
68
119
|
@app_name = @config.delete(:app_name)
|
120
|
+
@report_uri = @config.delete(:report_uri)
|
69
121
|
|
70
|
-
|
71
|
-
|
72
|
-
META.each do |meta|
|
73
|
-
self.send("#{meta}=", @config.delete(meta))
|
74
|
-
end
|
75
|
-
|
122
|
+
@disable_fill_missing = !!@config.delete(:disable_fill_missing)
|
76
123
|
@enforce = !!@config.delete(:enforce)
|
77
|
-
@tag_report_uri =
|
124
|
+
@tag_report_uri = !!@config.delete(:tag_report_uri)
|
125
|
+
@script_hashes = @config.delete(:script_hashes) || []
|
78
126
|
|
79
|
-
|
127
|
+
add_script_hashes if @script_hashes.any?
|
80
128
|
fill_directives unless disable_fill_missing?
|
81
129
|
end
|
82
130
|
|
131
|
+
##
|
132
|
+
# Return or initialize the nonce value used for this header.
|
133
|
+
# If a reference to a controller is passed in the config, this method
|
134
|
+
# will check if a nonce has already been set and use it.
|
135
|
+
def nonce
|
136
|
+
@nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
141
|
+
# "Content-Security-Policy-Report-Only"
|
83
142
|
def name
|
84
|
-
base =
|
85
|
-
if !@enforce
|
143
|
+
base = HEADER_NAME
|
144
|
+
if !@enforce
|
86
145
|
base += "-Report-Only"
|
87
146
|
end
|
88
147
|
base
|
89
148
|
end
|
90
149
|
|
150
|
+
##
|
151
|
+
# Return the value of the CSP header
|
91
152
|
def value
|
92
153
|
return @config if @config.is_a?(String)
|
93
154
|
if @config
|
@@ -99,26 +160,28 @@ module SecureHeaders
|
|
99
160
|
|
100
161
|
private
|
101
162
|
|
163
|
+
def add_script_hashes
|
164
|
+
@config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
|
165
|
+
end
|
166
|
+
|
102
167
|
def build_value
|
103
168
|
raise "Expected to find default_src directive value" unless @config[:default_src]
|
104
169
|
append_http_additions unless ssl_request?
|
105
170
|
header_value = [
|
106
|
-
generic_directives
|
171
|
+
generic_directives,
|
172
|
+
non_default_directives,
|
107
173
|
report_uri_directive
|
108
174
|
].join.strip
|
109
|
-
rescue StandardError => e
|
110
|
-
raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
|
111
175
|
end
|
112
176
|
|
113
177
|
def fill_directives
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
178
|
+
if default = @config[:default_src]
|
179
|
+
DIRECTIVES.each do |directive|
|
180
|
+
unless @config[directive]
|
181
|
+
@config[directive] = default
|
182
|
+
end
|
119
183
|
end
|
120
184
|
end
|
121
|
-
@config
|
122
185
|
end
|
123
186
|
|
124
187
|
def append_http_additions
|
@@ -129,75 +192,20 @@ module SecureHeaders
|
|
129
192
|
end
|
130
193
|
end
|
131
194
|
|
132
|
-
def normalize_csp_options
|
133
|
-
@config = @config.inject({}) do |hash, (key, value)|
|
134
|
-
# lambdas
|
135
|
-
config_val = value.respond_to?(:call) ? value.call : value
|
136
|
-
# space-delimeted strings
|
137
|
-
config_val = config_val.split if config_val.is_a? String
|
138
|
-
# array of strings
|
139
|
-
if config_val.respond_to?(:map) #skip booleans
|
140
|
-
config_val = config_val.map do |val|
|
141
|
-
translate_dir_value(val)
|
142
|
-
end.flatten.uniq
|
143
|
-
end
|
144
|
-
|
145
|
-
hash[key] = config_val
|
146
|
-
hash
|
147
|
-
end
|
148
|
-
|
149
|
-
@report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
|
150
|
-
end
|
151
|
-
|
152
|
-
# translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
|
153
195
|
def translate_dir_value val
|
154
196
|
if %w{inline eval}.include?(val)
|
155
197
|
val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
|
156
|
-
# self/none are special sources/src-dir-values and need to be quoted
|
198
|
+
# self/none are special sources/src-dir-values and need to be quoted
|
157
199
|
elsif %{self none}.include?(val)
|
158
200
|
"'#{val}'"
|
159
201
|
elsif val == 'nonce'
|
160
|
-
@controller
|
202
|
+
self.class.set_nonce(@controller, nonce)
|
161
203
|
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
162
204
|
else
|
163
205
|
val
|
164
206
|
end
|
165
207
|
end
|
166
208
|
|
167
|
-
# if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
|
168
|
-
# or only a path was supplied (in which case we assume cross-host)
|
169
|
-
# we need to forward the request for Firefox.
|
170
|
-
def normalize_reporting_endpoint
|
171
|
-
if @ua && @ua =~ /Firefox/
|
172
|
-
if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
|
173
|
-
return
|
174
|
-
end
|
175
|
-
|
176
|
-
if forward_endpoint
|
177
|
-
warn "[DEPRECATION] forwarder is removed in 2.0"
|
178
|
-
@report_uri = FF_CSP_ENDPOINT
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
if @tag_report_uri
|
183
|
-
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
184
|
-
@report_uri += "&app_name=#{@app_name}" if @app_name
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def same_origin?
|
189
|
-
return unless report_uri && request_uri
|
190
|
-
|
191
|
-
begin
|
192
|
-
origin = URI.parse(request_uri)
|
193
|
-
uri = URI.parse(report_uri)
|
194
|
-
rescue URI::InvalidURIError
|
195
|
-
return false
|
196
|
-
end
|
197
|
-
|
198
|
-
uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
|
199
|
-
end
|
200
|
-
|
201
209
|
def report_uri_directive
|
202
210
|
return '' if @report_uri.nil?
|
203
211
|
|
@@ -209,44 +217,40 @@ module SecureHeaders
|
|
209
217
|
end
|
210
218
|
end
|
211
219
|
|
220
|
+
if @tag_report_uri
|
221
|
+
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
222
|
+
@report_uri += "&app_name=#{@app_name}" if @app_name
|
223
|
+
end
|
224
|
+
|
212
225
|
"report-uri #{@report_uri};"
|
213
226
|
end
|
214
227
|
|
215
|
-
def generic_directives
|
228
|
+
def generic_directives
|
216
229
|
header_value = ''
|
217
|
-
if config[:img_src]
|
218
|
-
config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
|
230
|
+
if @config[:img_src]
|
231
|
+
@config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:')
|
219
232
|
else
|
220
|
-
config[:img_src] = config[:default_src] + ['data:']
|
233
|
+
@config[:img_src] = @config[:default_src] + ['data:']
|
221
234
|
end
|
222
235
|
|
223
|
-
|
224
|
-
|
225
|
-
header_value += build_directive(k)
|
236
|
+
DIRECTIVES.each do |directive_name|
|
237
|
+
header_value += build_directive(directive_name) if @config[directive_name]
|
226
238
|
end
|
227
239
|
|
228
240
|
header_value
|
229
241
|
end
|
230
242
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
243
|
+
def non_default_directives
|
244
|
+
header_value = ''
|
245
|
+
NON_DEFAULT_SOURCES.each do |directive_name|
|
246
|
+
header_value += build_directive(directive_name) if @config[directive_name]
|
247
|
+
end
|
235
248
|
|
236
|
-
|
237
|
-
sym.to_s.gsub('_', '-')
|
249
|
+
header_value
|
238
250
|
end
|
239
251
|
|
240
|
-
def
|
241
|
-
@
|
242
|
-
@ua = request.env['HTTP_USER_AGENT']
|
243
|
-
@request_uri = if request.respond_to?(:original_url)
|
244
|
-
# rails 3.1+
|
245
|
-
request.original_url
|
246
|
-
else
|
247
|
-
# rails 2/3.0
|
248
|
-
request.url
|
249
|
-
end
|
252
|
+
def build_directive(key)
|
253
|
+
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
|
250
254
|
end
|
251
255
|
end
|
252
256
|
end
|
@@ -16,26 +16,4 @@ else
|
|
16
16
|
include ::SecureHeaders
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
20
|
-
module SecureHeaders
|
21
|
-
module Routing
|
22
|
-
module MapperExtensions
|
23
|
-
def csp_endpoint
|
24
|
-
@set.add_route(ContentSecurityPolicy::FF_CSP_ENDPOINT, {:controller => "content_security_policy", :action => "scribe"})
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
if defined?(ActiveSupport::Dependencies)
|
31
|
-
if ActiveSupport::Dependencies.autoload_paths
|
32
|
-
ActiveSupport::Dependencies.autoload_paths << File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)
|
33
|
-
else
|
34
|
-
ActiveSupport::Dependencies.autoload_paths = [File.expand_path(File.join("..", "..", "..", "app", "controllers"), __FILE__)]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
if defined? ActionController::Routing
|
39
|
-
ActionController::Routing::RouteSet::Mapper.send :include, ::SecureHeaders::Routing::MapperExtensions
|
40
|
-
end
|
41
19
|
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
|
+
module Helpers #:nodoc:
|
66
|
+
include SecureHeaders::ViewHelpers
|
67
|
+
end
|
68
|
+
end
|
data/lib/secure_headers.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
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
|
6
9
|
|
7
10
|
def configure &block
|
8
11
|
instance_eval &block
|
12
|
+
if File.exists?(SCRIPT_HASH_CONFIG_FILE)
|
13
|
+
::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
|
14
|
+
end
|
9
15
|
end
|
10
16
|
end
|
11
17
|
end
|
@@ -33,6 +39,7 @@ module SecureHeaders
|
|
33
39
|
|
34
40
|
def ensure_security_headers options = {}
|
35
41
|
self.secure_headers_options = options
|
42
|
+
before_filter :prep_script_hash
|
36
43
|
before_filter :set_hsts_header
|
37
44
|
before_filter :set_x_frame_options_header
|
38
45
|
before_filter :set_csp_header
|
@@ -49,7 +56,6 @@ module SecureHeaders
|
|
49
56
|
end
|
50
57
|
|
51
58
|
module InstanceMethods
|
52
|
-
# Re-added for backwards compat.
|
53
59
|
def set_security_headers(options = self.class.secure_headers_options)
|
54
60
|
set_csp_header(request, options[:csp])
|
55
61
|
set_hsts_header(options[:hsts])
|
@@ -59,28 +65,54 @@ module SecureHeaders
|
|
59
65
|
set_x_download_options_header(options[:x_download_options])
|
60
66
|
end
|
61
67
|
|
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
68
|
# set_csp_header - uses the request accessor and SecureHeader::Configuration settings
|
65
69
|
# set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
|
66
70
|
# set_csp_header(+Hash+) - uses the request accessor and options from parameters
|
67
71
|
# set_csp_header(+Rack::Request+, +Hash+)
|
68
|
-
def set_csp_header(req = nil,
|
69
|
-
|
70
|
-
|
71
|
-
options = req
|
72
|
+
def set_csp_header(req = nil, config=nil)
|
73
|
+
if req.is_a?(Hash) || req.is_a?(FalseClass)
|
74
|
+
config = req
|
72
75
|
end
|
73
76
|
|
74
|
-
|
75
|
-
|
77
|
+
config = self.class.secure_headers_options[:csp] if config.nil?
|
78
|
+
config = self.class.options_for :csp, config
|
76
79
|
|
77
|
-
return if
|
80
|
+
return if config == false
|
81
|
+
|
82
|
+
if config && config[:script_hash_middleware]
|
83
|
+
ContentSecurityPolicy.add_to_env(request, self, config)
|
84
|
+
else
|
85
|
+
csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
|
86
|
+
set_header(csp_header)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def prep_script_hash
|
92
|
+
if ::SecureHeaders::Configuration.script_hashes
|
93
|
+
@script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
|
94
|
+
ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
|
95
|
+
save_hash_for_later payload
|
96
|
+
end
|
78
97
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
98
|
+
ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
|
99
|
+
save_hash_for_later payload
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def save_hash_for_later payload
|
105
|
+
matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
|
106
|
+
|
107
|
+
if payload[:layout]
|
108
|
+
# 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
|
109
|
+
layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
|
110
|
+
|
111
|
+
matching_hashes << layout_hashes if layout_hashes
|
112
|
+
end
|
113
|
+
|
114
|
+
if matching_hashes.any?
|
115
|
+
request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
|
84
116
|
end
|
85
117
|
end
|
86
118
|
|
@@ -126,7 +158,7 @@ module SecureHeaders
|
|
126
158
|
end
|
127
159
|
end
|
128
160
|
|
129
|
-
|
161
|
+
|
130
162
|
require "secure_headers/version"
|
131
163
|
require "secure_headers/header"
|
132
164
|
require "secure_headers/headers/content_security_policy"
|
@@ -136,3 +168,5 @@ require "secure_headers/headers/x_xss_protection"
|
|
136
168
|
require "secure_headers/headers/x_content_type_options"
|
137
169
|
require "secure_headers/headers/x_download_options"
|
138
170
|
require "secure_headers/railtie"
|
171
|
+
require "secure_headers/hash_helper"
|
172
|
+
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
|