DEV: Automatically extend CSP when themes link to external scripts (#9531)

DEV: Automatically extend CSP when themes link to external scripts (#9531)

diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb
index ed0966f..5754900 100644
--- a/app/models/theme_field.rb
+++ b/app/models/theme_field.rb
@@ -317,6 +317,7 @@ class ThemeField < ActiveRecord::Base
       self.value_baked, self.error = translation_field? ? process_translation : process_html(self.value)
       self.error = nil unless self.error.present?
       self.compiler_version = COMPILER_VERSION
+      CSP::Extension.clear_theme_extensions_cache!
     elsif extra_js_field?
       self.value_baked, self.error = process_extra_js(self.value)
       self.error = nil unless self.error.present?
diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb
index 5dd7022..f7b49b2 100644
--- a/lib/content_security_policy/extension.rb
+++ b/lib/content_security_policy/extension.rb
@@ -42,7 +42,9 @@ class ContentSecurityPolicy
     def find_theme_extensions(theme_ids)
       extensions = []
 
-      Theme.where(id: Theme.transform_ids(theme_ids)).find_each do |theme|
+      resolved_ids = Theme.transform_ids(theme_ids)
+
+      Theme.where(id: resolved_ids).find_each do |theme|
         theme.cached_settings.each do |setting, value|
           extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
         end
@@ -50,6 +52,21 @@ class ContentSecurityPolicy
 
       extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions)
 
+      html_fields = ThemeField.where(
+        theme_id: resolved_ids,
+        target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
+        name: ThemeField.html_fields
+      )
+
+      auto_script_src_extension = { script_src: [] }
+      html_fields.each(&:ensure_baked!)
+      doc = html_fields.map(&:value_baked).join("\n")
+      Nokogiri::HTML.fragment(doc).css('script[src]').each do |node|
+        auto_script_src_extension[:script_src] << node['src']
+      end
+
+      extensions << auto_script_src_extension
+
       extensions
     end
 
diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb
index 68a5dc2..d9eb978 100644
--- a/spec/lib/content_security_policy_spec.rb
+++ b/spec/lib/content_security_policy_spec.rb
@@ -212,6 +212,19 @@ describe ContentSecurityPolicy do
       expect(parse(theme_policy)['script-src']).to_not include('https://from-theme-flag.script')
       expect(parse(theme_policy)['worker-src']).to_not include('from-theme-flag.worker')
     end
+
+    it 'is extended automatically when themes reference external scripts' do
+      policy # call this first to make sure further actions clear the cache
+
+      theme.set_field(target: :common, name: "header", value: "<script src='https://example.com/myscript.js'/>")
+      theme.save!
+
+      expect(parse(theme_policy)['script-src']).to include('https://example.com/myscript.js')
+
+      theme.destroy!
+
+      expect(parse(theme_policy)['script-src']).to_not include('https://example.com/myscript.js')
+    end
   end
 
   it 'can be extended by site setting' do

GitHub sha: 8a112b74

This commit appears in #9531 which was approved by eviltrout. It was merged by davidtaylorhq.