33require 'securerandom'
44require 'user_agent_parser'
55require 'json'
6+ require 'pry'
67
78module SecureHeaders
89 class ContentSecurityPolicyBuildError < StandardError ; end
@@ -11,28 +12,62 @@ module Constants
1112 DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
1213 HEADER_NAME = "Content-Security-Policy"
1314 ENV_KEY = 'secure_headers.content_security_policy'
14- DIRECTIVES = [
15+
16+ DIRECTIVES_1_0 = [
1517 :default_src ,
1618 :connect_src ,
1719 :font_src ,
1820 :frame_src ,
1921 :img_src ,
2022 :media_src ,
2123 :object_src ,
24+ :sandbox ,
2225 :script_src ,
2326 :style_src ,
24- :base_uri ,
27+ :report_uri
28+ ] . freeze
29+
30+ DIRECTIVES_2_0 = [
31+ DIRECTIVES_1_0 ,
32+ :base_url ,
2533 :child_src ,
2634 :form_action ,
2735 :frame_ancestors ,
2836 :plugin_types
29- ]
37+ ] . flatten . freeze
3038
31- OTHER = [
32- :report_uri
33- ]
3439
35- ALL_DIRECTIVES = DIRECTIVES + OTHER
40+ # All the directives currently under consideration for CSP level 3.
41+ # https://w3c.github.io/webappsec/specs/CSP2/
42+ DIRECTIVES_3_0 = [
43+ DIRECTIVES_2_0 ,
44+ :manifest_src ,
45+ :reflected_xss
46+ ] . flatten . freeze
47+
48+ # All the directives that are not currently in a formal spec, but have
49+ # been implemented somewhere.
50+ DIRECTIVES_DRAFT = [
51+ :block_all_mixed_content ,
52+ ] . freeze
53+
54+ SAFARI_DIRECTIVES = DIRECTIVES_1_0
55+
56+ FIREFOX_UNSUPPORTED_DIRECTIVES = [
57+ :block_all_mixed_content ,
58+ :child_src ,
59+ :plugin_types
60+ ] . freeze
61+
62+ FIREFOX_DIRECTIVES = (
63+ DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
64+ ) . freeze
65+
66+ CHROME_DIRECTIVES = (
67+ DIRECTIVES_2_0 + DIRECTIVES_DRAFT
68+ ) . freeze
69+
70+ ALL_DIRECTIVES = DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT
3671 CONFIG_KEY = :csp
3772 end
3873
@@ -99,33 +134,36 @@ def initialize(config=nil, options={})
99134 @ua = options [ :ua ]
100135 @ssl_request = !!options . delete ( :ssl )
101136 @request_uri = options . delete ( :request_uri )
137+ @http_additions = config . delete ( :http_additions )
138+ @app_name = config . delete ( :app_name )
139+ @enforce = !!config . delete ( :enforce )
140+ @disable_img_src_data_uri = !!config . delete ( :disable_img_src_data_uri )
141+ @tag_report_uri = !!config . delete ( :tag_report_uri )
142+ @script_hashes = config . delete ( :script_hashes ) || [ ]
102143
103144 # Config values can be string, array, or lamdba values
104145 @config = config . inject ( { } ) do |hash , ( key , value ) |
105146 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
147+ if ContentSecurityPolicy ::ALL_DIRECTIVES . include? ( key . to_sym ) # directives need to be normalized to arrays of strings
108148 config_val = config_val . split if config_val . is_a? String
109149 if config_val . is_a? ( Array )
110150 config_val = config_val . map do |val |
111151 translate_dir_value ( val )
112152 end . flatten . uniq
113153 end
154+ else
155+ raise ArgumentError . new ( "Unknown directive supplied: #{ key } " )
114156 end
115157
158+
116159 hash [ key ] = config_val
117160 hash
118161 end
119162
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 ) || [ ]
127-
128163 add_script_hashes if @script_hashes . any?
164+ puts @config
165+ strip_unsupported_directives
166+ puts @config
129167 end
130168
131169 ##
@@ -160,13 +198,20 @@ def value
160198
161199 def to_json
162200 build_value
163- @config . to_json . gsub ( /(\w +)_src/ , "\\ 1-src" )
201+ out = @config . inject ( { } ) do |hash , ( key , value ) |
202+ hash [ key . to_s . gsub ( /(\w +)_(\w +)/ , "\\ 1-\\ 2" ) ] = value
203+ hash
204+ end
205+ puts out
206+ out . to_json
164207 end
165208
166209 def self . from_json ( *json_configs )
167210 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 )
211+ config = JSON . parse ( one_config ) . inject ( { } ) do |hash , ( key , value ) |
212+ hash [ key . gsub ( /(\w +)-(\w +)/ , "\\ 1_\\ 2" ) . to_sym ] = value
213+ hash
214+ end
170215 combined_config . merge ( config ) do |_ , lhs , rhs |
171216 lhs | rhs
172217 end
@@ -182,10 +227,7 @@ def add_script_hashes
182227 def build_value
183228 raise "Expected to find default_src directive value" unless @config [ :default_src ]
184229 append_http_additions unless ssl_request?
185- header_value = [
186- generic_directives ,
187- report_uri_directive
188- ] . join . strip
230+ generic_directives
189231 end
190232
191233 def append_http_additions
@@ -204,7 +246,7 @@ def translate_dir_value val
204246 warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead."
205247 "'#{ val } '"
206248 elsif val == 'nonce'
207- if supports_nonces? ( @ua )
249+ if supports_nonces?
208250 self . class . set_nonce ( @controller , nonce )
209251 [ "'nonce-#{ nonce } '" , "'unsafe-inline'" ]
210252 else
@@ -215,25 +257,6 @@ def translate_dir_value val
215257 end
216258 end
217259
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-
237260 def generic_directives
238261 header_value = ''
239262 data_uri = @disable_img_src_data_uri ? [ ] : [ "data:" ]
@@ -243,7 +266,7 @@ def generic_directives
243266 @config [ :img_src ] = @config [ :default_src ] + data_uri
244267 end
245268
246- DIRECTIVES . each do |directive_name |
269+ ALL_DIRECTIVES . each do |directive_name |
247270 header_value += build_directive ( directive_name ) if @config [ directive_name ]
248271 end
249272
@@ -254,8 +277,25 @@ def build_directive(key)
254277 "#{ self . class . symbol_to_hyphen_case ( key ) } #{ @config [ key ] . join ( " " ) } ; "
255278 end
256279
257- def supports_nonces? ( user_agent )
258- parsed_ua = UserAgentParser . parse ( user_agent )
280+ def strip_unsupported_directives
281+ @config . select! { |key , _ | supported_directives . include? ( key ) }
282+ end
283+
284+ def supported_directives
285+ @supported_directives ||= case UserAgentParser . parse ( @ua ) . family
286+ when "Chrome"
287+ CHROME_DIRECTIVES
288+ when "Safari"
289+ SAFARI_DIRECTIVES
290+ when "Firefox"
291+ FIREFOX_DIRECTIVES
292+ else
293+ DIRECTIVES_1_0
294+ end
295+ end
296+
297+ def supports_nonces?
298+ parsed_ua = UserAgentParser . parse ( @ua )
259299 [ "Chrome" , "Opera" , "Firefox" ] . include? ( parsed_ua . family )
260300 end
261301 end
0 commit comments