@@ -11,28 +11,62 @@ module Constants
11
11
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
12
12
HEADER_NAME = "Content-Security-Policy"
13
13
ENV_KEY = 'secure_headers.content_security_policy'
14
- DIRECTIVES = [
14
+
15
+ DIRECTIVES_1_0 = [
15
16
:default_src ,
16
17
:connect_src ,
17
18
:font_src ,
18
19
:frame_src ,
19
20
:img_src ,
20
21
:media_src ,
21
22
:object_src ,
23
+ :sandbox ,
22
24
:script_src ,
23
25
:style_src ,
26
+ :report_uri
27
+ ] . freeze
28
+
29
+ DIRECTIVES_2_0 = [
30
+ DIRECTIVES_1_0 ,
24
31
:base_uri ,
25
32
:child_src ,
26
33
:form_action ,
27
34
:frame_ancestors ,
28
35
:plugin_types
29
- ]
36
+ ] . flatten . freeze
30
37
31
- OTHER = [
32
- :report_uri
33
- ]
34
38
35
- ALL_DIRECTIVES = DIRECTIVES + OTHER
39
+ # All the directives currently under consideration for CSP level 3.
40
+ # https://w3c.github.io/webappsec/specs/CSP2/
41
+ DIRECTIVES_3_0 = [
42
+ DIRECTIVES_2_0 ,
43
+ :manifest_src ,
44
+ :reflected_xss
45
+ ] . flatten . freeze
46
+
47
+ # All the directives that are not currently in a formal spec, but have
48
+ # been implemented somewhere.
49
+ DIRECTIVES_DRAFT = [
50
+ :block_all_mixed_content ,
51
+ ] . freeze
52
+
53
+ SAFARI_DIRECTIVES = DIRECTIVES_1_0
54
+
55
+ FIREFOX_UNSUPPORTED_DIRECTIVES = [
56
+ :block_all_mixed_content ,
57
+ :child_src ,
58
+ :plugin_types
59
+ ] . freeze
60
+
61
+ FIREFOX_DIRECTIVES = (
62
+ DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
63
+ ) . freeze
64
+
65
+ CHROME_DIRECTIVES = (
66
+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT
67
+ ) . freeze
68
+
69
+ ALL_DIRECTIVES = [ DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT ] . flatten . uniq . sort
36
70
CONFIG_KEY = :csp
37
71
end
38
72
@@ -99,33 +133,55 @@ def initialize(config=nil, options={})
99
133
@ua = options [ :ua ]
100
134
@ssl_request = !!options . delete ( :ssl )
101
135
@request_uri = options . delete ( :request_uri )
136
+ @http_additions = config . delete ( :http_additions )
137
+ @disable_img_src_data_uri = !!config . delete ( :disable_img_src_data_uri )
138
+ @tag_report_uri = !!config . delete ( :tag_report_uri )
139
+ @script_hashes = config . delete ( :script_hashes ) || [ ]
140
+ @app_name = config . delete ( :app_name )
141
+ @app_name = @app_name . call ( @controller ) if @app_name . respond_to? ( :call )
142
+ @enforce = config . delete ( :enforce )
143
+ @enforce = @enforce . call ( @controller ) if @enforce . respond_to? ( :call )
144
+ @enforce = !!@enforce
102
145
103
146
# Config values can be string, array, or lamdba values
104
147
@config = config . inject ( { } ) do |hash , ( key , value ) |
105
148
config_val = value . respond_to? ( :call ) ? value . call ( @controller ) : value
106
-
107
- if DIRECTIVES . include? ( key ) # directives need to be normalized to arrays of strings
149
+ if ALL_DIRECTIVES . include? ( key . to_sym ) # directives need to be normalized to arrays of strings
108
150
config_val = config_val . split if config_val . is_a? String
109
151
if config_val . is_a? ( Array )
110
152
config_val = config_val . map do |val |
111
153
translate_dir_value ( val )
112
154
end . flatten . uniq
113
155
end
156
+ elsif key != :script_hash_middleware
157
+ raise ArgumentError . new ( "Unknown directive supplied: #{ key } " )
114
158
end
115
159
116
160
hash [ key ] = config_val
117
161
hash
118
162
end
119
163
120
- @http_additions = @config . delete ( :http_additions )
121
- @app_name = @config . delete ( :app_name )
122
- @report_uri = @config . delete ( :report_uri )
123
- @enforce = !!@config . delete ( :enforce )
124
- @disable_img_src_data_uri = !!@config . delete ( :disable_img_src_data_uri )
125
- @tag_report_uri = !!@config . delete ( :tag_report_uri )
126
- @script_hashes = @config . delete ( :script_hashes ) || [ ]
164
+ # normalize and tag the report-uri
165
+ if @config [ :report_uri ]
166
+ @config [ :report_uri ] = @config [ :report_uri ] . map do |report_uri |
167
+ if report_uri . start_with? ( '//' )
168
+ report_uri = if @ssl_request
169
+ "https:" + report_uri
170
+ else
171
+ "http:" + report_uri
172
+ end
173
+ end
174
+
175
+ if @tag_report_uri
176
+ report_uri = "#{ report_uri } ?enforce=#{ @enforce } "
177
+ report_uri += "&app_name=#{ @app_name } " if @app_name
178
+ end
179
+ report_uri
180
+ end
181
+ end
127
182
128
183
add_script_hashes if @script_hashes . any?
184
+ strip_unsupported_directives
129
185
end
130
186
131
187
##
@@ -160,13 +216,20 @@ def value
160
216
161
217
def to_json
162
218
build_value
163
- @config . to_json . gsub ( /(\w +)_src/ , "\\ 1-src" )
219
+ @config . inject ( { } ) do |hash , ( key , value ) |
220
+ if ALL_DIRECTIVES . include? ( key )
221
+ hash [ key . to_s . gsub ( /(\w +)_(\w +)/ , "\\ 1-\\ 2" ) ] = value
222
+ end
223
+ hash
224
+ end . to_json
164
225
end
165
226
166
227
def self . from_json ( *json_configs )
167
228
json_configs . inject ( { } ) do |combined_config , one_config |
168
- one_config = one_config . gsub ( /(\w +)-src/ , "\\ 1_src" )
169
- config = JSON . parse ( one_config , :symbolize_names => true )
229
+ config = JSON . parse ( one_config ) . inject ( { } ) do |hash , ( key , value ) |
230
+ hash [ key . gsub ( /(\w +)-(\w +)/ , "\\ 1_\\ 2" ) . to_sym ] = value
231
+ hash
232
+ end
170
233
combined_config . merge ( config ) do |_ , lhs , rhs |
171
234
lhs | rhs
172
235
end
@@ -182,10 +245,7 @@ def add_script_hashes
182
245
def build_value
183
246
raise "Expected to find default_src directive value" unless @config [ :default_src ]
184
247
append_http_additions unless ssl_request?
185
- header_value = [
186
- generic_directives ,
187
- report_uri_directive
188
- ] . join . strip
248
+ generic_directives
189
249
end
190
250
191
251
def append_http_additions
@@ -204,7 +264,7 @@ def translate_dir_value val
204
264
warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead."
205
265
"'#{ val } '"
206
266
elsif val == 'nonce'
207
- if supports_nonces? ( @ua )
267
+ if supports_nonces?
208
268
self . class . set_nonce ( @controller , nonce )
209
269
[ "'nonce-#{ nonce } '" , "'unsafe-inline'" ]
210
270
else
@@ -215,47 +275,50 @@ def translate_dir_value val
215
275
end
216
276
end
217
277
218
- def report_uri_directive
219
- return '' if @report_uri . nil?
220
-
221
- if @report_uri . start_with? ( '//' )
222
- @report_uri = if @ssl_request
223
- "https:" + @report_uri
224
- else
225
- "http:" + @report_uri
226
- end
227
- end
228
-
229
- if @tag_report_uri
230
- @report_uri = "#{ @report_uri } ?enforce=#{ @enforce } "
231
- @report_uri += "&app_name=#{ @app_name } " if @app_name
232
- end
233
-
234
- "report-uri #{ @report_uri } ;"
235
- end
236
-
278
+ # ensures defualt_src is first and report_uri is last
237
279
def generic_directives
238
- header_value = ''
280
+ header_value = build_directive ( :default_src )
239
281
data_uri = @disable_img_src_data_uri ? [ ] : [ "data:" ]
240
282
if @config [ :img_src ]
241
283
@config [ :img_src ] = @config [ :img_src ] + data_uri unless @config [ :img_src ] . include? ( 'data:' )
242
284
else
243
285
@config [ :img_src ] = @config [ :default_src ] + data_uri
244
286
end
245
287
246
- DIRECTIVES . each do |directive_name |
247
- header_value += build_directive ( directive_name ) if @config [ directive_name ]
288
+ ( ALL_DIRECTIVES - [ :default_src , :report_uri ] ) . each do |directive_name |
289
+ if @config [ directive_name ]
290
+ header_value += build_directive ( directive_name )
291
+ end
248
292
end
249
293
250
- header_value
294
+ header_value += build_directive ( :report_uri ) if @config [ :report_uri ]
295
+
296
+ header_value . strip
251
297
end
252
298
253
299
def build_directive ( key )
254
300
"#{ self . class . symbol_to_hyphen_case ( key ) } #{ @config [ key ] . join ( " " ) } ; "
255
301
end
256
302
257
- def supports_nonces? ( user_agent )
258
- parsed_ua = UserAgentParser . parse ( user_agent )
303
+ def strip_unsupported_directives
304
+ @config . select! { |key , _ | supported_directives . include? ( key ) }
305
+ end
306
+
307
+ def supported_directives
308
+ @supported_directives ||= case UserAgentParser . parse ( @ua ) . family
309
+ when "Chrome"
310
+ CHROME_DIRECTIVES
311
+ when "Safari"
312
+ SAFARI_DIRECTIVES
313
+ when "Firefox"
314
+ FIREFOX_DIRECTIVES
315
+ else
316
+ DIRECTIVES_1_0
317
+ end
318
+ end
319
+
320
+ def supports_nonces?
321
+ parsed_ua = UserAgentParser . parse ( @ua )
259
322
[ "Chrome" , "Opera" , "Firefox" ] . include? ( parsed_ua . family )
260
323
end
261
324
end
0 commit comments