secure_headers 2.5.3 → 3.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 +4 -4
- data/.rspec +1 -0
- data/.travis.yml +2 -1
- data/Gemfile +9 -16
- data/README.md +154 -331
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +341 -254
- data/lib/secure_headers/headers/public_key_pins.rb +43 -58
- data/lib/secure_headers/headers/strict_transport_security.rb +21 -49
- data/lib/secure_headers/headers/x_content_type_options.rb +18 -33
- data/lib/secure_headers/headers/x_download_options.rb +18 -33
- data/lib/secure_headers/headers/x_frame_options.rb +24 -34
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -34
- data/lib/secure_headers/headers/x_xss_protection.rb +17 -48
- data/lib/secure_headers/middleware.rb +15 -0
- data/lib/secure_headers/padrino.rb +1 -2
- data/lib/secure_headers/railtie.rb +9 -6
- data/lib/secure_headers/view_helper.rb +27 -43
- data/lib/secure_headers.rb +254 -61
- data/secure_headers.gemspec +7 -12
- data/spec/lib/secure_headers/configuration_spec.rb +80 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +111 -276
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +11 -18
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -30
- data/spec/lib/secure_headers/middleware_spec.rb +40 -0
- data/spec/lib/secure_headers_spec.rb +201 -339
- data/spec/spec_helper.rb +30 -30
- data/upgrading-to-3-0.md +35 -0
- metadata +14 -100
- data/fixtures/rails_3_2_22/.rspec +0 -1
- data/fixtures/rails_3_2_22/Gemfile +0 -6
- data/fixtures/rails_3_2_22/README.rdoc +0 -261
- data/fixtures/rails_3_2_22/Rakefile +0 -7
- data/fixtures/rails_3_2_22/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_3_2_22/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_3_2_22/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22/config/application.rb +0 -14
- data/fixtures/rails_3_2_22/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +0 -16
- data/fixtures/rails_3_2_22/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22/config/script_hashes.yml +0 -5
- data/fixtures/rails_3_2_22/config.ru +0 -7
- data/fixtures/rails_3_2_22/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22/spec/spec_helper.rb +0 -15
- data/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/.rspec +0 -1
- data/fixtures/rails_3_2_22_no_init/Gemfile +0 -6
- data/fixtures/rails_3_2_22_no_init/README.rdoc +0 -261
- data/fixtures/rails_3_2_22_no_init/Rakefile +0 -7
- data/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +0 -20
- data/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb +0 -12
- data/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb +0 -0
- data/fixtures/rails_3_2_22_no_init/config/application.rb +0 -17
- data/fixtures/rails_3_2_22_no_init/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22_no_init/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22_no_init/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/config.ru +0 -4
- data/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
- data/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_4_1_8/Gemfile +0 -5
- data/fixtures/rails_4_1_8/README.rdoc +0 -28
- data/fixtures/rails_4_1_8/Rakefile +0 -6
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
- 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 +0 -11
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_4_1_8/config/application.rb +0 -15
- data/fixtures/rails_4_1_8/config/boot.rb +0 -4
- data/fixtures/rails_4_1_8/config/environment.rb +0 -5
- data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -16
- data/fixtures/rails_4_1_8/config/routes.rb +0 -4
- data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
- data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
- data/fixtures/rails_4_1_8/config.ru +0 -4
- 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 +0 -83
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
- 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/controller_extension.rb +0 -158
- data/lib/secure_headers/hash_helper.rb +0 -7
- data/lib/secure_headers/header.rb +0 -5
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
- data/lib/secure_headers/version.rb +0 -3
- data/lib/tasks/tasks.rake +0 -48
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -46
@@ -1,334 +1,421 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'base64'
|
3
3
|
require 'securerandom'
|
4
|
-
require 'user_agent_parser'
|
5
4
|
require 'json'
|
6
5
|
|
7
6
|
module SecureHeaders
|
8
|
-
class
|
9
|
-
class ContentSecurityPolicy
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
7
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
8
|
+
class ContentSecurityPolicy
|
9
|
+
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
10
|
+
DEFAULT_VALUE = "default-src https:".freeze
|
11
|
+
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
12
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
13
|
+
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
14
|
+
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
15
|
+
DATA_PROTOCOL = "data:".freeze
|
16
|
+
SELF = "'self'".freeze
|
17
|
+
NONE = "'none'".freeze
|
18
|
+
STAR = "*".freeze
|
19
|
+
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
20
|
+
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
21
|
+
|
22
|
+
# leftover deprecated values that will be in common use upon upgrading.
|
23
|
+
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
24
|
+
|
25
|
+
DEFAULT_SRC = :default_src
|
26
|
+
CONNECT_SRC = :connect_src
|
27
|
+
FONT_SRC = :font_src
|
28
|
+
FRAME_SRC = :frame_src
|
29
|
+
IMG_SRC = :img_src
|
30
|
+
MEDIA_SRC = :media_src
|
31
|
+
OBJECT_SRC = :object_src
|
32
|
+
SANDBOX = :sandbox
|
33
|
+
SCRIPT_SRC = :script_src
|
34
|
+
STYLE_SRC = :style_src
|
35
|
+
REPORT_URI = :report_uri
|
36
|
+
|
37
|
+
DIRECTIVES_1_0 = [
|
38
|
+
DEFAULT_SRC,
|
39
|
+
CONNECT_SRC,
|
40
|
+
FONT_SRC,
|
41
|
+
FRAME_SRC,
|
42
|
+
IMG_SRC,
|
43
|
+
MEDIA_SRC,
|
44
|
+
OBJECT_SRC,
|
45
|
+
SANDBOX,
|
46
|
+
SCRIPT_SRC,
|
47
|
+
STYLE_SRC,
|
48
|
+
REPORT_URI
|
49
|
+
].freeze
|
50
|
+
|
51
|
+
BASE_URI = :base_uri
|
52
|
+
CHILD_SRC = :child_src
|
53
|
+
FORM_ACTION = :form_action
|
54
|
+
FRAME_ANCESTORS = :frame_ancestors
|
55
|
+
PLUGIN_TYPES = :plugin_types
|
56
|
+
|
57
|
+
DIRECTIVES_2_0 = [
|
58
|
+
DIRECTIVES_1_0,
|
59
|
+
BASE_URI,
|
60
|
+
CHILD_SRC,
|
61
|
+
FORM_ACTION,
|
62
|
+
FRAME_ANCESTORS,
|
63
|
+
PLUGIN_TYPES
|
64
|
+
].flatten.freeze
|
65
|
+
|
66
|
+
# All the directives currently under consideration for CSP level 3.
|
67
|
+
# https://w3c.github.io/webappsec/specs/CSP2/
|
68
|
+
MANIFEST_SRC = :manifest_src
|
69
|
+
REFLECTED_XSS = :reflected_xss
|
70
|
+
DIRECTIVES_3_0 = [
|
71
|
+
DIRECTIVES_2_0,
|
72
|
+
MANIFEST_SRC,
|
73
|
+
REFLECTED_XSS
|
74
|
+
].flatten.freeze
|
75
|
+
|
76
|
+
# All the directives that are not currently in a formal spec, but have
|
77
|
+
# been implemented somewhere.
|
78
|
+
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
79
|
+
DIRECTIVES_DRAFT = [
|
80
|
+
BLOCK_ALL_MIXED_CONTENT
|
81
|
+
].freeze
|
82
|
+
|
83
|
+
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
84
|
+
|
85
|
+
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
86
|
+
BLOCK_ALL_MIXED_CONTENT,
|
87
|
+
CHILD_SRC,
|
88
|
+
PLUGIN_TYPES
|
89
|
+
].freeze
|
90
|
+
|
91
|
+
FIREFOX_DIRECTIVES = (
|
92
|
+
DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
93
|
+
).freeze
|
94
|
+
|
95
|
+
CHROME_DIRECTIVES = (
|
96
|
+
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
97
|
+
).freeze
|
98
|
+
|
99
|
+
ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
|
100
|
+
|
101
|
+
# Think of default-src and report-uri as the beginning and end respectively,
|
102
|
+
# everything else is in between.
|
103
|
+
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
104
|
+
|
105
|
+
VARIATIONS = {
|
106
|
+
"Chrome" => CHROME_DIRECTIVES,
|
107
|
+
"Opera" => CHROME_DIRECTIVES,
|
108
|
+
"Firefox" => FIREFOX_DIRECTIVES,
|
109
|
+
"Safari" => SAFARI_DIRECTIVES,
|
110
|
+
"Other" => CHROME_DIRECTIVES
|
111
|
+
}.freeze
|
112
|
+
|
113
|
+
OTHER = "Other".freeze
|
114
|
+
|
115
|
+
DIRECTIVE_VALUE_TYPES = {
|
116
|
+
BASE_URI => :source_list,
|
117
|
+
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
118
|
+
CHILD_SRC => :source_list,
|
119
|
+
CONNECT_SRC => :source_list,
|
120
|
+
DEFAULT_SRC => :source_list,
|
121
|
+
FONT_SRC => :source_list,
|
122
|
+
FORM_ACTION => :source_list,
|
123
|
+
FRAME_ANCESTORS => :source_list,
|
124
|
+
FRAME_SRC => :source_list,
|
125
|
+
IMG_SRC => :source_list,
|
126
|
+
MANIFEST_SRC => :source_list,
|
127
|
+
MEDIA_SRC => :source_list,
|
128
|
+
OBJECT_SRC => :source_list,
|
129
|
+
PLUGIN_TYPES => :source_list,
|
130
|
+
REFLECTED_XSS => :string,
|
131
|
+
REPORT_URI => :source_list,
|
132
|
+
SANDBOX => :string,
|
133
|
+
SCRIPT_SRC => :source_list,
|
134
|
+
STYLE_SRC => :source_list
|
135
|
+
}.freeze
|
136
|
+
|
137
|
+
CONFIG_KEY = :csp
|
138
|
+
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
139
|
+
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
140
|
+
|
141
|
+
WILDCARD_SOURCES = [
|
142
|
+
UNSAFE_EVAL,
|
143
|
+
UNSAFE_INLINE,
|
144
|
+
STAR
|
145
|
+
]
|
78
146
|
|
79
147
|
class << self
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
def
|
85
|
-
|
148
|
+
# Public: generate a header name, value array that is user-agent-aware.
|
149
|
+
#
|
150
|
+
# Returns a default policy if no configuration is provided, or a
|
151
|
+
# header name and value based on the config.
|
152
|
+
def make_header(config, user_agent)
|
153
|
+
header = new(config, user_agent)
|
154
|
+
[header.name, header.value]
|
86
155
|
end
|
87
156
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
157
|
+
# Public: Validates that the configuration has a valid type, or that it is a valid
|
158
|
+
# source expression.
|
159
|
+
#
|
160
|
+
# Private: validates that a source expression:
|
161
|
+
# 1. has a valid name
|
162
|
+
# 2. is an array of strings
|
163
|
+
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
164
|
+
#
|
165
|
+
# Does not validate the invididual values of the source expression (e.g.
|
166
|
+
# script_src => h*t*t*p: will not raise an exception)
|
167
|
+
def validate_config!(config)
|
168
|
+
return if config.nil? || config == OPT_OUT
|
169
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
|
170
|
+
config.each do |key, value|
|
171
|
+
if key == :report_only
|
172
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
173
|
+
else
|
174
|
+
validate_directive!(key, value)
|
175
|
+
end
|
176
|
+
end
|
95
177
|
end
|
96
178
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
179
|
+
# Public: combine the values from two different configs.
|
180
|
+
#
|
181
|
+
# original - the main config
|
182
|
+
# additions - values to be merged in
|
183
|
+
#
|
184
|
+
# raises an error if the original config is OPT_OUT
|
185
|
+
#
|
186
|
+
# 1. for non-source-list values (report_only, block_all_mixed_content),
|
187
|
+
# additions will overwrite the original value.
|
188
|
+
# 2. if a value in additions does not exist in the original config, the
|
189
|
+
# default-src value is included to match original behavior.
|
190
|
+
# 3. if a value in additions does exist in the original config, the two
|
191
|
+
# values are joined.
|
192
|
+
def combine_policies(original, additions)
|
193
|
+
if original == OPT_OUT
|
194
|
+
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
195
|
+
end
|
104
196
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
# rails 2/3.0
|
111
|
-
request.url
|
197
|
+
# in case we would be appending to an empty directive, fill it with the default-src value
|
198
|
+
additions.keys.each do |directive|
|
199
|
+
unless original[directive] || !source_list?(directive)
|
200
|
+
original[directive] = original[:default_src]
|
201
|
+
end
|
112
202
|
end
|
113
|
-
end
|
114
203
|
|
115
|
-
|
116
|
-
|
204
|
+
# merge the two hashes. combine (instead of overwrite) the array values
|
205
|
+
# when each hash contains a value for a given key.
|
206
|
+
original.merge(additions) do |directive, lhs, rhs|
|
207
|
+
if source_list?(directive)
|
208
|
+
lhs | rhs
|
209
|
+
else
|
210
|
+
rhs
|
211
|
+
end
|
212
|
+
end
|
117
213
|
end
|
118
|
-
end
|
119
214
|
|
120
|
-
|
121
|
-
# :controller used for setting instance variables for nonces/hashes
|
122
|
-
# :ssl_request used to determine if http_additions should be used
|
123
|
-
# :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
|
124
|
-
#
|
125
|
-
# :report used to determine what :ssl_request, :ua, and :request_uri are set to
|
126
|
-
def initialize(config=nil, options={})
|
127
|
-
return unless config
|
215
|
+
private
|
128
216
|
|
129
|
-
|
130
|
-
|
217
|
+
def source_list?(directive)
|
218
|
+
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
131
219
|
end
|
132
220
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
config_val = if value.respond_to?(:call)
|
141
|
-
warn "[DEPRECATION] secure_headers 3.x will not support procs as config values."
|
142
|
-
value.call(@controller)
|
143
|
-
else
|
144
|
-
value
|
145
|
-
end
|
146
|
-
|
147
|
-
if ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings
|
148
|
-
if config_val.is_a? String
|
149
|
-
warn "[DEPRECATION] A String was supplied for directive #{key}. secure_headers 3.x will require all directives to be arrays of strings."
|
150
|
-
config_val = config_val.split
|
221
|
+
# Private: Validates that the configuration has a valid type, or that it is a valid
|
222
|
+
# source expression.
|
223
|
+
def validate_directive!(key, value)
|
224
|
+
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
|
225
|
+
when :boolean
|
226
|
+
unless boolean?(value)
|
227
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
|
151
228
|
end
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
end.flatten.uniq
|
229
|
+
when :string
|
230
|
+
unless value.is_a?(String)
|
231
|
+
raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
|
156
232
|
end
|
233
|
+
else
|
234
|
+
validate_source_expression!(key, value)
|
157
235
|
end
|
158
|
-
|
159
|
-
hash[key] = config_val
|
160
|
-
hash
|
161
236
|
end
|
162
237
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
"https:" + report_uri
|
179
|
-
else
|
180
|
-
"http:" + report_uri
|
181
|
-
end
|
182
|
-
end
|
238
|
+
# Private: validates that a source expression:
|
239
|
+
# 1. has a valid name
|
240
|
+
# 2. is an array of strings
|
241
|
+
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
242
|
+
#
|
243
|
+
# Does not validate the invididual values of the source expression (e.g.
|
244
|
+
# script_src => h*t*t*p: will not raise an exception)
|
245
|
+
def validate_source_expression!(key, value)
|
246
|
+
# source expressions
|
247
|
+
unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
|
248
|
+
raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
|
249
|
+
end
|
250
|
+
unless value.is_a?(Array) && value.all? { |v| v.is_a?(String) }
|
251
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings")
|
252
|
+
end
|
183
253
|
|
184
|
-
|
185
|
-
|
186
|
-
|
254
|
+
value.each do |source_expression|
|
255
|
+
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
|
256
|
+
raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
|
187
257
|
end
|
188
|
-
report_uri
|
189
258
|
end
|
190
259
|
end
|
191
260
|
|
192
|
-
|
193
|
-
|
261
|
+
def boolean?(value)
|
262
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
263
|
+
end
|
194
264
|
end
|
195
265
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
266
|
+
|
267
|
+
def initialize(config = nil, user_agent = OTHER)
|
268
|
+
config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
|
269
|
+
@config = config
|
270
|
+
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
271
|
+
user_agent
|
272
|
+
else
|
273
|
+
UserAgent.parse(user_agent)
|
274
|
+
end
|
275
|
+
@report_only = !!@config[:report_only]
|
276
|
+
@script_nonce = @config[:script_nonce]
|
277
|
+
@style_nonce = @config[:style_nonce]
|
202
278
|
end
|
203
279
|
|
204
280
|
##
|
205
281
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
206
282
|
# "Content-Security-Policy-Report-Only"
|
207
283
|
def name
|
208
|
-
|
209
|
-
|
210
|
-
|
284
|
+
if @report_only
|
285
|
+
REPORT_ONLY
|
286
|
+
else
|
287
|
+
HEADER_NAME
|
211
288
|
end
|
212
|
-
base
|
213
289
|
end
|
214
290
|
|
215
291
|
##
|
216
292
|
# Return the value of the CSP header
|
217
293
|
def value
|
218
|
-
|
219
|
-
if @config
|
294
|
+
@value ||= if @config
|
220
295
|
build_value
|
221
296
|
else
|
222
|
-
|
297
|
+
DEFAULT_VALUE
|
223
298
|
end
|
224
299
|
end
|
225
300
|
|
226
|
-
|
227
|
-
build_value
|
228
|
-
@config.inject({}) do |hash, (key, value)|
|
229
|
-
if ALL_DIRECTIVES.include?(key)
|
230
|
-
hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value
|
231
|
-
end
|
232
|
-
hash
|
233
|
-
end.to_json
|
234
|
-
end
|
301
|
+
private
|
235
302
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
303
|
+
# Private: converts the config object into a string representing a policy.
|
304
|
+
# Places default-src at the first directive and report-uri as the last. All
|
305
|
+
# others are presented in alphabetical order.
|
306
|
+
#
|
307
|
+
# Unsupported directives are filtered based on the user agent.
|
308
|
+
#
|
309
|
+
# Returns a content security policy header value.
|
310
|
+
def build_value
|
311
|
+
directives.map do |directive_name|
|
312
|
+
case DIRECTIVE_VALUE_TYPES[directive_name]
|
313
|
+
when :boolean
|
314
|
+
symbol_to_hyphen_case(directive_name)
|
315
|
+
when :string
|
316
|
+
[symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
|
317
|
+
else
|
318
|
+
build_directive(directive_name)
|
244
319
|
end
|
245
|
-
end
|
320
|
+
end.join("; ")
|
246
321
|
end
|
247
322
|
|
248
|
-
|
323
|
+
# Private: builds a string that represents one directive in a minified form.
|
324
|
+
# If a directive contains *, all other values are omitted.
|
325
|
+
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
326
|
+
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
327
|
+
#
|
328
|
+
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
329
|
+
#
|
330
|
+
# Returns a string representing a directive.
|
331
|
+
def build_directive(directive_name)
|
332
|
+
source_list = @config[directive_name].compact
|
249
333
|
|
250
|
-
|
251
|
-
|
252
|
-
|
334
|
+
value = if source_list.include?(STAR)
|
335
|
+
# Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
|
336
|
+
source_list.select { |value| WILDCARD_SOURCES.include?(value) }
|
337
|
+
else
|
338
|
+
populate_nonces(directive_name, source_list)
|
253
339
|
|
254
|
-
|
255
|
-
|
256
|
-
append_http_additions unless ssl_request?
|
257
|
-
generic_directives
|
258
|
-
end
|
340
|
+
# Discard any 'none' values if more directives are supplied since none may override values.
|
341
|
+
source_list.reject! { |value| value == NONE } if source_list.length > 1
|
259
342
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
@config[k] ||= []
|
264
|
-
@config[k] << v
|
343
|
+
# remove schemes and dedup source expressions
|
344
|
+
source_list = strip_source_schemes(source_list) unless directive_name == REPORT_URI
|
345
|
+
dedup_source_list(source_list).join(" ")
|
265
346
|
end
|
347
|
+
[symbol_to_hyphen_case(directive_name), value].join(" ")
|
266
348
|
end
|
267
349
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
else
|
280
|
-
"'unsafe-inline'"
|
350
|
+
# Removes duplicates and sources that already match an existing wild card.
|
351
|
+
#
|
352
|
+
# e.g. *.github.com asdf.github.com becomes *.github.com
|
353
|
+
def dedup_source_list(sources)
|
354
|
+
sources = sources.uniq
|
355
|
+
wild_sources = sources.select { |source| source =~ STAR_REGEXP }
|
356
|
+
|
357
|
+
if wild_sources.any?
|
358
|
+
sources.reject do |source|
|
359
|
+
!wild_sources.include?(source) &&
|
360
|
+
wild_sources.any? { |pattern| File.fnmatch(pattern, source) }
|
281
361
|
end
|
282
362
|
else
|
283
|
-
|
363
|
+
sources
|
284
364
|
end
|
285
365
|
end
|
286
366
|
|
287
|
-
#
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
367
|
+
# Private: append a nonce to the script/style directories if script_nonce
|
368
|
+
# or style_nonce are provided.
|
369
|
+
def populate_nonces(directive, source_list)
|
370
|
+
case directive
|
371
|
+
when SCRIPT_SRC
|
372
|
+
append_nonce(source_list, @script_nonce)
|
373
|
+
when STYLE_SRC
|
374
|
+
append_nonce(source_list, @style_nonce)
|
295
375
|
end
|
376
|
+
end
|
296
377
|
|
297
|
-
|
298
|
-
|
299
|
-
|
378
|
+
# Private: adds a nonce or 'unsafe-inline' depending on browser support.
|
379
|
+
# If a nonce is populated, inline content is assumed.
|
380
|
+
#
|
381
|
+
# While CSP is backward compatible in that a policy with a nonce will ignore
|
382
|
+
# unsafe-inline, this is more concise.
|
383
|
+
def append_nonce(source_list, nonce)
|
384
|
+
if nonce
|
385
|
+
if nonces_supported?
|
386
|
+
source_list << "'nonce-#{nonce}'"
|
387
|
+
else
|
388
|
+
source_list << UNSAFE_INLINE
|
300
389
|
end
|
301
390
|
end
|
302
|
-
|
303
|
-
header_value += build_directive(:report_uri) if @config[:report_uri]
|
304
|
-
|
305
|
-
header_value.strip
|
306
391
|
end
|
307
392
|
|
308
|
-
|
309
|
-
|
393
|
+
# Private: return the list of directives that are supported by the user agent,
|
394
|
+
# starting with default-src and ending with report-uri.
|
395
|
+
def directives
|
396
|
+
[DEFAULT_SRC,
|
397
|
+
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
398
|
+
REPORT_URI].flatten.select { |directive| @config.key?(directive) }
|
310
399
|
end
|
311
400
|
|
312
|
-
|
313
|
-
|
401
|
+
# Private: Remove scheme from source expressions.
|
402
|
+
def strip_source_schemes(source_list)
|
403
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
314
404
|
end
|
315
405
|
|
406
|
+
# Private: determine which directives are supported for the given user agent.
|
407
|
+
#
|
408
|
+
# Returns an array of symbols representing the directives.
|
316
409
|
def supported_directives
|
317
|
-
@supported_directives ||=
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
when "Firefox"
|
323
|
-
FIREFOX_DIRECTIVES
|
324
|
-
else
|
325
|
-
DIRECTIVES_1_0
|
326
|
-
end
|
410
|
+
@supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
|
411
|
+
end
|
412
|
+
|
413
|
+
def nonces_supported?
|
414
|
+
@nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
|
327
415
|
end
|
328
416
|
|
329
|
-
def
|
330
|
-
|
331
|
-
["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
|
417
|
+
def symbol_to_hyphen_case(sym)
|
418
|
+
sym.to_s.tr('_', '-')
|
332
419
|
end
|
333
420
|
end
|
334
421
|
end
|