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.
- 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 +184 -101
- 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 +154 -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 +69 -68
- 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
|
@@ -1,79 +1,154 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require 'securerandom'
|
|
2
4
|
|
|
3
5
|
module SecureHeaders
|
|
4
6
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
5
7
|
class ContentSecurityPolicy < Header
|
|
6
8
|
module Constants
|
|
7
|
-
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
DIRECTIVES = [
|
|
11
|
-
|
|
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
|
|
12
35
|
end
|
|
13
36
|
include Constants
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
|
|
17
|
-
|
|
18
|
-
alias :disable_chrome_extension? :disable_chrome_extension
|
|
38
|
+
attr_reader :disable_fill_missing, :ssl_request
|
|
19
39
|
alias :disable_fill_missing? :disable_fill_missing
|
|
20
40
|
alias :ssl_request? :ssl_request
|
|
21
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
|
+
|
|
22
83
|
# +options+ param contains
|
|
23
|
-
# :
|
|
84
|
+
# :controller used for setting instance variables for nonces/hashes
|
|
24
85
|
# :ssl_request used to determine if http_additions should be used
|
|
25
|
-
# :request_uri used to determine if firefox should send the report directly
|
|
26
|
-
# or use the forwarding endpoint
|
|
27
86
|
# :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
|
|
28
87
|
#
|
|
29
88
|
# :report used to determine what :ssl_request, :ua, and :request_uri are set to
|
|
30
89
|
def initialize(config=nil, options={})
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
return unless config
|
|
91
|
+
|
|
33
92
|
if options[:request]
|
|
34
|
-
|
|
35
|
-
else
|
|
36
|
-
@ua = options[:ua]
|
|
37
|
-
# fails open, assumes http. Bad idea? Will always include http additions.
|
|
38
|
-
# could also fail if not supplied.
|
|
39
|
-
@ssl_request = !!options.delete(:ssl)
|
|
40
|
-
# a nil value here means we always assume we are not on the same host,
|
|
41
|
-
# which causes all FF csp reports to go through the forwarder
|
|
42
|
-
@request_uri = options.delete(:request_uri)
|
|
93
|
+
options = options.merge(self.class.options_from_request(options[:request]))
|
|
43
94
|
end
|
|
44
95
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
@controller = options[:controller]
|
|
97
|
+
@ua = options[:ua]
|
|
98
|
+
@ssl_request = !!options.delete(:ssl)
|
|
99
|
+
@request_uri = options.delete(:request_uri)
|
|
100
|
+
|
|
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
|
|
104
|
+
|
|
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
|
|
56
113
|
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
hash[key] = config_val
|
|
115
|
+
hash
|
|
59
116
|
end
|
|
60
117
|
|
|
118
|
+
@http_additions = @config.delete(:http_additions)
|
|
119
|
+
@app_name = @config.delete(:app_name)
|
|
61
120
|
@report_uri = @config.delete(:report_uri)
|
|
62
|
-
@script_nonce = @config.delete(:script_nonce)
|
|
63
121
|
|
|
64
|
-
|
|
65
|
-
|
|
122
|
+
@disable_fill_missing = !!@config.delete(:disable_fill_missing)
|
|
123
|
+
@enforce = !!@config.delete(:enforce)
|
|
124
|
+
@tag_report_uri = !!@config.delete(:tag_report_uri)
|
|
125
|
+
@script_hashes = @config.delete(:script_hashes) || []
|
|
126
|
+
|
|
127
|
+
add_script_hashes if @script_hashes.any?
|
|
66
128
|
fill_directives unless disable_fill_missing?
|
|
67
129
|
end
|
|
68
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"
|
|
69
142
|
def name
|
|
70
|
-
base =
|
|
71
|
-
if
|
|
143
|
+
base = HEADER_NAME
|
|
144
|
+
if !@enforce
|
|
72
145
|
base += "-Report-Only"
|
|
73
146
|
end
|
|
74
147
|
base
|
|
75
148
|
end
|
|
76
149
|
|
|
150
|
+
##
|
|
151
|
+
# Return the value of the CSP header
|
|
77
152
|
def value
|
|
78
153
|
return @config if @config.is_a?(String)
|
|
79
154
|
if @config
|
|
@@ -85,87 +160,52 @@ module SecureHeaders
|
|
|
85
160
|
|
|
86
161
|
private
|
|
87
162
|
|
|
163
|
+
def add_script_hashes
|
|
164
|
+
@config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
|
|
165
|
+
end
|
|
166
|
+
|
|
88
167
|
def build_value
|
|
89
168
|
raise "Expected to find default_src directive value" unless @config[:default_src]
|
|
90
169
|
append_http_additions unless ssl_request?
|
|
91
170
|
header_value = [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
script_nonce_directive,
|
|
97
|
-
].join
|
|
98
|
-
|
|
99
|
-
#store the value for next time
|
|
100
|
-
@config = header_value
|
|
101
|
-
header_value.strip
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
|
|
171
|
+
generic_directives,
|
|
172
|
+
non_default_directives,
|
|
173
|
+
report_uri_directive
|
|
174
|
+
].join.strip
|
|
104
175
|
end
|
|
105
176
|
|
|
106
177
|
def fill_directives
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
178
|
+
if default = @config[:default_src]
|
|
179
|
+
DIRECTIVES.each do |directive|
|
|
180
|
+
unless @config[directive]
|
|
181
|
+
@config[directive] = default
|
|
182
|
+
end
|
|
112
183
|
end
|
|
113
184
|
end
|
|
114
|
-
@config
|
|
115
185
|
end
|
|
116
186
|
|
|
117
187
|
def append_http_additions
|
|
118
|
-
return unless http_additions
|
|
119
|
-
http_additions.each do |k, v|
|
|
188
|
+
return unless @http_additions
|
|
189
|
+
@http_additions.each do |k, v|
|
|
120
190
|
@config[k] ||= []
|
|
121
191
|
@config[k] << v
|
|
122
192
|
end
|
|
123
193
|
end
|
|
124
194
|
|
|
125
|
-
def normalize_csp_options
|
|
126
|
-
@config.each do |k,v|
|
|
127
|
-
@config[k] = v.split if v.is_a? String
|
|
128
|
-
@config[k] = @config[k].map do |val|
|
|
129
|
-
translate_dir_value(val)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
|
|
135
195
|
def translate_dir_value val
|
|
136
196
|
if %w{inline eval}.include?(val)
|
|
137
197
|
val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
|
|
138
|
-
# 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
|
|
139
199
|
elsif %{self none}.include?(val)
|
|
140
200
|
"'#{val}'"
|
|
201
|
+
elsif val == 'nonce'
|
|
202
|
+
self.class.set_nonce(@controller, nonce)
|
|
203
|
+
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
141
204
|
else
|
|
142
205
|
val
|
|
143
206
|
end
|
|
144
207
|
end
|
|
145
208
|
|
|
146
|
-
# if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
|
|
147
|
-
# or only a path was supplied (in which case we assume cross-host)
|
|
148
|
-
# we need to forward the request for Firefox.
|
|
149
|
-
def normalize_reporting_endpoint
|
|
150
|
-
if @ua && @ua =~ /Firefox/
|
|
151
|
-
if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
|
|
152
|
-
return
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
if forward_endpoint
|
|
156
|
-
@report_uri = FF_CSP_ENDPOINT
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def same_origin?
|
|
162
|
-
return unless report_uri && request_uri
|
|
163
|
-
|
|
164
|
-
origin = URI.parse(request_uri)
|
|
165
|
-
uri = URI.parse(report_uri)
|
|
166
|
-
uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
|
|
167
|
-
end
|
|
168
|
-
|
|
169
209
|
def report_uri_directive
|
|
170
210
|
return '' if @report_uri.nil?
|
|
171
211
|
|
|
@@ -177,55 +217,40 @@ module SecureHeaders
|
|
|
177
217
|
end
|
|
178
218
|
end
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
|
|
220
|
+
if @tag_report_uri
|
|
221
|
+
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
|
222
|
+
@report_uri += "&app_name=#{@app_name}" if @app_name
|
|
223
|
+
end
|
|
182
224
|
|
|
183
|
-
|
|
184
|
-
return '' if @script_nonce.nil?
|
|
185
|
-
nonce_value = if @script_nonce.is_a?(String)
|
|
186
|
-
@script_nonce
|
|
187
|
-
elsif @controller
|
|
188
|
-
@controller.instance_exec(&@script_nonce)
|
|
189
|
-
else
|
|
190
|
-
@script_nonce.call
|
|
191
|
-
end
|
|
192
|
-
"script-nonce #{nonce_value};"
|
|
225
|
+
"report-uri #{@report_uri};"
|
|
193
226
|
end
|
|
194
227
|
|
|
195
|
-
def generic_directives
|
|
228
|
+
def generic_directives
|
|
196
229
|
header_value = ''
|
|
197
|
-
if config[:img_src]
|
|
198
|
-
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:')
|
|
199
232
|
else
|
|
200
|
-
config[:img_src] = ['data:']
|
|
233
|
+
@config[:img_src] = @config[:default_src] + ['data:']
|
|
201
234
|
end
|
|
202
235
|
|
|
203
|
-
|
|
204
|
-
header_value += build_directive(
|
|
236
|
+
DIRECTIVES.each do |directive_name|
|
|
237
|
+
header_value += build_directive(directive_name) if @config[directive_name]
|
|
205
238
|
end
|
|
206
239
|
|
|
207
240
|
header_value
|
|
208
241
|
end
|
|
209
242
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
214
248
|
|
|
215
|
-
|
|
216
|
-
sym.to_s.gsub('_', '-')
|
|
249
|
+
header_value
|
|
217
250
|
end
|
|
218
251
|
|
|
219
|
-
def
|
|
220
|
-
@
|
|
221
|
-
@ua = request.env['HTTP_USER_AGENT']
|
|
222
|
-
@request_uri = if request.respond_to?(:original_url)
|
|
223
|
-
# rails 3.1+
|
|
224
|
-
request.original_url
|
|
225
|
-
else
|
|
226
|
-
# rails 2/3.0
|
|
227
|
-
request.url
|
|
228
|
-
end
|
|
252
|
+
def build_directive(key)
|
|
253
|
+
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
|
|
229
254
|
end
|
|
230
255
|
end
|
|
231
256
|
end
|
|
@@ -6,7 +6,7 @@ module SecureHeaders
|
|
|
6
6
|
HSTS_HEADER_NAME = 'Strict-Transport-Security'
|
|
7
7
|
HSTS_MAX_AGE = "631138519"
|
|
8
8
|
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
|
|
9
|
-
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?\z/i
|
|
9
|
+
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
|
|
10
10
|
MESSAGE = "The config value supplied for the HSTS header was invalid."
|
|
11
11
|
end
|
|
12
12
|
include Constants
|
|
@@ -31,6 +31,7 @@ module SecureHeaders
|
|
|
31
31
|
max_age = @config.fetch(:max_age, HSTS_MAX_AGE)
|
|
32
32
|
value = "max-age=" + max_age.to_s
|
|
33
33
|
value += "; includeSubdomains" if @config[:include_subdomains]
|
|
34
|
+
value += "; preload" if @config[:preload]
|
|
34
35
|
|
|
35
36
|
value
|
|
36
37
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class XDOBuildError < StandardError; end
|
|
3
|
+
class XDownloadOptions < Header
|
|
4
|
+
module Constants
|
|
5
|
+
XDO_HEADER_NAME = "X-Download-Options"
|
|
6
|
+
DEFAULT_VALUE = 'noopen'
|
|
7
|
+
end
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize(config = nil)
|
|
11
|
+
@config = config
|
|
12
|
+
validate_config unless @config.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
XDO_HEADER_NAME
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value
|
|
20
|
+
case @config
|
|
21
|
+
when NilClass
|
|
22
|
+
DEFAULT_VALUE
|
|
23
|
+
when String
|
|
24
|
+
@config
|
|
25
|
+
else
|
|
26
|
+
@config[:value]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_config
|
|
33
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
|
34
|
+
unless value.casecmp(DEFAULT_VALUE) == 0
|
|
35
|
+
raise XDOBuildError.new("Value can only be nil or 'noopen'")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class XPCDPBuildError < StandardError; end
|
|
3
|
+
class XPermittedCrossDomainPolicies < Header
|
|
4
|
+
module Constants
|
|
5
|
+
XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
|
|
6
|
+
DEFAULT_VALUE = 'none'
|
|
7
|
+
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
|
|
8
|
+
end
|
|
9
|
+
include Constants
|
|
10
|
+
|
|
11
|
+
def initialize(config = nil)
|
|
12
|
+
@config = config
|
|
13
|
+
validate_config unless @config.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name
|
|
17
|
+
XPCDP_HEADER_NAME
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def value
|
|
21
|
+
case @config
|
|
22
|
+
when NilClass
|
|
23
|
+
DEFAULT_VALUE
|
|
24
|
+
when String
|
|
25
|
+
@config
|
|
26
|
+
else
|
|
27
|
+
@config[:value]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_config
|
|
34
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
|
35
|
+
unless VALID_POLICIES.include?(value.downcase)
|
|
36
|
+
raise XPCDPBuildError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -4,7 +4,7 @@ module SecureHeaders
|
|
|
4
4
|
module Constants
|
|
5
5
|
X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection'
|
|
6
6
|
DEFAULT_VALUE = "1"
|
|
7
|
-
VALID_X_XSS_HEADER = /\A[01](; mode=block)?\z/i
|
|
7
|
+
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
|
|
8
8
|
end
|
|
9
9
|
include Constants
|
|
10
10
|
|
|
@@ -26,6 +26,7 @@ module SecureHeaders
|
|
|
26
26
|
else
|
|
27
27
|
value = @config[:value].to_s
|
|
28
28
|
value += "; mode=#{@config[:mode]}" if @config[:mode]
|
|
29
|
+
value += "; report=#{@config[:report_uri]}" if @config[:report_uri]
|
|
29
30
|
value
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module Padrino
|
|
3
|
+
class << self
|
|
4
|
+
##
|
|
5
|
+
# Main class that register this extension.
|
|
6
|
+
#
|
|
7
|
+
def registered(app)
|
|
8
|
+
app.extend SecureHeaders::ClassMethods
|
|
9
|
+
app.helpers SecureHeaders::InstanceMethods
|
|
10
|
+
end
|
|
11
|
+
alias :included :registered
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -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
|
+
module Helpers #:nodoc:
|
|
66
|
+
include SecureHeaders::ViewHelpers
|
|
67
|
+
end
|
|
68
|
+
end
|