FEATURE: Disallow unsafe CSP if plugin is enabled (#155)

FEATURE: Disallow unsafe CSP if plugin is enabled (#155)

This plugin adds validation encrypt_enabled and CSP site settings to make it impossible to enable unsafe CSP settings and the plugin.

Unsafe CSP is a security risk for the plugin because it makes it easier for potential attackers to inject malicious code that can steal keys or encrypted conversations.

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index a752a5e..c89e132 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -4,8 +4,12 @@ en:
     enabled_already: "You have already enabled encrypted messages."
     only_pms: "Only private messages can be encrypted."
     no_encrypt_keys: "Something went wrong. No encryption keys were included in the payload."
+
   site_settings:
     encrypt_enabled: "Enable encrypted private messages."
-    auto_enable_encrypt: "Automatically enable encrypt for all logged in users"
+    auto_enable_encrypt: "Automatically enable encrypt for all logged in users."
     encrypt_groups: "The name of groups that are able to use encryption (empty means everyone)."
-    encrypt_pms_default: "Encrypt all new private messages by default"
+    encrypt_pms_default: "Encrypt all new private messages by default."
+
+    errors:
+      encrypt_unsafe_csp: "Unsafe CSP directives like 'unsafe-eval' and 'unsafe-inline' cannot be used when the Discourse Encrypt plugin is enabled. "
diff --git a/config/settings.yml b/config/settings.yml
index 68aeab0..163ab14 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -2,6 +2,7 @@ plugins:
   encrypt_enabled:
     default: true
     refresh: true
+    validator: "EncryptEnabledValidator"
   auto_enable_encrypt:
     client: true
     default: false
diff --git a/lib/site_settings_type_supervisor_extensions.rb b/lib/site_settings_type_supervisor_extensions.rb
new file mode 100644
index 0000000..f0ac069
--- /dev/null
+++ b/lib/site_settings_type_supervisor_extensions.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module DiscourseEncrypt::SiteSettingsTypeSupervisorExtensions
+  def validate_content_security_policy(value)
+    super if defined?(super)
+
+    if value == 't' && !DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src) && SiteSetting.encrypt_enabled
+      raise Discourse::InvalidParameters.new(I18n.t('site_settings.errors.encrypt_unsafe_csp'))
+    end
+  end
+
+  def validate_content_security_policy_script_src(value)
+    super if defined?(super)
+
+    if SiteSetting.content_security_policy? && !DiscourseEncrypt.safe_csp_src?(value) && SiteSetting.encrypt_enabled
+      raise Discourse::InvalidParameters.new(I18n.t('site_settings.errors.encrypt_unsafe_csp'))
+    end
+  end
+end
diff --git a/lib/validators/encrypt_enabled_validator.rb b/lib/validators/encrypt_enabled_validator.rb
new file mode 100644
index 0000000..73f8b09
--- /dev/null
+++ b/lib/validators/encrypt_enabled_validator.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class EncryptEnabledValidator
+  def initialize(opts = {})
+  end
+
+  def valid_value?(value)
+    !SiteSetting.content_security_policy? || DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src) || value == 'f'
+  end
+
+  def error_message
+    I18n.t('site_settings.errors.encrypt_unsafe_csp')
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index 72ce745..5e194c9 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -15,9 +15,15 @@ register_asset "stylesheets/colors.scss", :color_definitions
 
 Rails.configuration.filter_parameters << :encrypt_private
 
+require_relative 'lib/validators/encrypt_enabled_validator.rb'
+
 after_initialize do
   module ::DiscourseEncrypt
     PLUGIN_NAME = 'discourse-encrypt'
+
+    def self.safe_csp_src?(value)
+      !value.include?("'unsafe-inline'") && !value.include?("'unsafe-eval'")
+    end
   end
 
   require_relative 'app/controllers/encrypt_controller.rb'
@@ -37,6 +43,7 @@ after_initialize do
   require_relative 'lib/post_actions_controller_extensions.rb'
   require_relative 'lib/post_extensions.rb'
   require_relative 'lib/site_setting_extensions.rb'
+  require_relative 'lib/site_settings_type_supervisor_extensions.rb'
   require_relative 'lib/topic_extensions.rb'
   require_relative 'lib/topic_guardian_extensions.rb'
   require_relative 'lib/topic_view_serializer_extension.rb'
@@ -73,6 +80,7 @@ after_initialize do
     GroupedSearchResultSerializer.class_eval { prepend DiscourseEncrypt::GroupedSearchResultSerializerExtension }
     Post.class_eval                          { prepend DiscourseEncrypt::PostExtensions }
     PostActionsController.class_eval         { prepend DiscourseEncrypt::PostActionsControllerExtensions }
+    SiteSettings::TypeSupervisor.class_eval  { prepend DiscourseEncrypt::SiteSettingsTypeSupervisorExtensions }
     Topic.class_eval                         { prepend DiscourseEncrypt::TopicExtensions }
     TopicGuardian.class_eval                 { prepend DiscourseEncrypt::TopicGuardianExtension }
     TopicsController.class_eval              { prepend DiscourseEncrypt::TopicsControllerExtensions }
@@ -91,6 +99,12 @@ after_initialize do
     end
   end
 
+  AdminDashboardData.add_problem_check do
+    if SiteSetting.content_security_policy? && !DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src) && SiteSetting.encrypt_enabled?
+      I18n.t('site_settings.errors.encrypt_unsafe_csp')
+    end
+  end
+
   TopicList.on_preload do |topics, topic_list|
     if SiteSetting.encrypt_enabled? && topics.size > 0 && topic_list.current_user
       topic_ids = topics.map(&:id)
diff --git a/spec/plugin_spec.rb b/spec/plugin_spec.rb
index dedd9b7..dd1a197 100644
--- a/spec/plugin_spec.rb
+++ b/spec/plugin_spec.rb
@@ -15,4 +15,21 @@ describe ::DiscourseEncrypt do
     expect(post.post_uploads.size).to eq(1)
     expect(post.post_uploads.first.upload).to eq(upload)
   end
+
+  it 'can enable encrypt if safe CSP' do
+    SiteSetting.encrypt_enabled = false # plugin is enabled by default
+    SiteSetting.content_security_policy_script_src = "default-src 'self' cdn.example.com|script-src 'self' js.example.com|style-src 'self' css.example.com"
+    expect { SiteSetting.encrypt_enabled = true }.not_to raise_error(Discourse::InvalidParameters)
+  end
+
+  it 'cannot enable encrypt if unsafe CSP' do
+    SiteSetting.encrypt_enabled = false # plugin is enabled by default
+    SiteSetting.content_security_policy_script_src = "'unsafe-eval'|'unsafe-inline'"
+    expect { SiteSetting.encrypt_enabled = true }.to raise_error(Discourse::InvalidParameters)
+  end
+
+  it 'cannot have unsafe CSP if encrypt is enabled' do
+    SiteSetting.encrypt_enabled = true
+    expect { SiteSetting.content_security_policy_script_src = "'unsafe-eval'|'unsafe-inline'" }.to raise_error(Discourse::InvalidParameters)
+  end
 end

GitHub sha: e1bf5f57c41602c20682c2cb218628bc73d4bebe

This commit appears in #155 which was approved by pmusaraj. It was merged by udan11.