Skip to content

Commit

Permalink
Add Content Security Policy support (decidim#10700)
Browse files Browse the repository at this point in the history
* Add Content Security Policy concern

* Add inclusion of Concern

* Add CSP settings

* Refactor

* Extract CSP in own class

* Running linters, add Headers module

* Add documentation, value to initializer

* Fix failing specs

* Apply review Recommendations

* Update the documentation links

* Apply suggestions from code review

Co-authored-by: Andrés Pereira de Lucena <[email protected]>

* Remove jsdeliver

* Apply review recommendations

* Update the documentation readme

* Running linters

* Revert jsdeliver.net

* Fix invalid rule spec

* Fixing code review bug

* Apply suggestions from code review

Co-authored-by: Andrés Pereira de Lucena <[email protected]>

* Comply with the latest changes

* Change the documentation

* Lock sass-embedded

* Add guard clause

---------

Co-authored-by: Andrés Pereira de Lucena <[email protected]>
  • Loading branch information
alecslupu and andreslucena authored Jun 26, 2023
1 parent c7db070 commit 2a12210
Show file tree
Hide file tree
Showing 36 changed files with 709 additions and 44 deletions.
27 changes: 27 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,33 @@ bundle exec rake decidim:initiatives:upgrade:fix_broken_pages

You can see more details about this change on PR [\#10928](https://github.com/decidim/decidim/pull/10928)

### 3.7. Add Content Security Policy (CSP) support

We have introduced support for Content Security Policy (CSP). This is a security feature that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks.
By default, the CSP is enabled, and is configured to be as restrictive as possible, having the following default configuration:

```ruby
{
"default-src" => %w('self' 'unsafe-inline'),
"script-src" => %w('self' 'unsafe-inline' 'unsafe-eval'),
"style-src" => %w('self' 'unsafe-inline'),
"img-src" => %w('self' *.hereapi.com data:),
"font-src" => %w('self'),
"connect-src" => %w('self' *.hereapi.com *.jsdelivr.net),
"frame-src" => %w('self'),
"media-src" => %w('self')
}
```

In order to customize the CSP we are providing, have 2 options, either by using a configuration key the initializer `config/initializers/decidim.rb` or by setting values in the Organization's system admin.

Please read more in the docs:

- [Customize Content Security Policy](https://docs.decidim.org/develop/en/customize/content_security_policy)
- [Using Content Security Policy initializer](https://docs.decidim.org/en/develop/configure/initializer#_content_security_policy)

You can check more about the implementation in the [\#10700](https://github.com/decidim/decidim/pull/10700) pull request.

## 4. Scheduled tasks

Implementers need to configure these changes it in your scheduler task system in the production server. We give the examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class ApplicationController < ::DecidimController
include LocaleSwitcher
include UseOrganizationTimeZone
include PayloadInfo
include HttpCachingDisabler
include Headers::HttpCachingDisabler
include Headers::ContentSecurityPolicy
include DisableRedirectionToExternalHost

include DisabledRedesignLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
<% if Decidim.enable_html_header_snippets %>
<div class="row column">
<%= form.text_area :header_snippets %>
<p class="help-text"><%= t(".header_snippets_help") %></p>
<p class="help-text"><%= t(".header_snippets_help_html") %></p>
</div>
<% end %>
</div>
Expand Down
2 changes: 1 addition & 1 deletion decidim-admin/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ en:
colors_title: Organization colors
colors_warning_html: Warning! Changing these colors can break the accessibility contrasts. You can check the contrast of your choosing with <a href="https://webaim.org/resources/contrastchecker">WebAIM Contrast Checker</a> or other similar tools.
explanation: This tool helps you out to choose a color scheme, made up of hues equally spaced around the color wheel, that will be used in the organization's website.
header_snippets_help: Use this field to add things to the HTML head. The most common use is to integrate third-party services that require some extra JavaScript or CSS. Also, you can use it to add extra meta tags to the HTML. Note that this will only be rendered in public pages, not in the admin section.
header_snippets_help_html: Use this field to add things to the HTML head. The most common use is to integrate third-party services that require some extra JavaScript or CSS. Also, you can use it to add extra meta tags to the HTML. Note that this will only be rendered in public pages, not in the admin section. If the code interacts with external APIs or does not comply with the application security guidelines, it will be necessary to change the Content Security Policy. Read more about <a href="https://docs.decidim.org/develop/en/customize/content_security_policy">on the documentation site</a> .
legend_html: Main application colors, based on the <a href="https://www.color-meanings.com/triadic-colors/">Triadic algorithm</a>. The <i>secondary</i> color is auto-calculated from the primary color and the saturation you chose.
saturation: Saturation
title: Color chooser
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "active_support/concern"

module Decidim
module Headers
module ContentSecurityPolicy
extend ActiveSupport::Concern

included do
def content_security_policy
@content_security_policy ||= Decidim::ContentSecurityPolicy.new(current_organization, Decidim.content_security_policies_extra)
end

after_action :append_content_security_policy_headers
end

private

def append_content_security_policy_headers
response.headers["Content-Security-Policy"] = content_security_policy.output_policy
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "active_support/concern"

module Decidim
module Headers
# This module will disable http caching from the controller in
# order to prevent proxies from storing sensible information.
module HttpCachingDisabler
extend ActiveSupport::Concern

included do
before_action :disable_http_caching
end

def disable_http_caching
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
response.cache_control.replace(no_cache: true, extras: ["no-store"])
end
end
end
end

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class ApplicationController < ::DecidimController
include ImpersonateUsers
include HasStoredPath
include NeedsTosAccepted
include HttpCachingDisabler
include Headers::HttpCachingDisabler
include Headers::ContentSecurityPolicy
include ActionAuthorization
include ForceAuthentication
include SafeRedirect
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddContentPolicyToDecidimOrganizations < ActiveRecord::Migration[6.1]
def change
add_column :decidim_organizations, :content_security_policy, :jsonb, default: {}
end
end
132 changes: 132 additions & 0 deletions decidim-core/lib/decidim/content_security_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

module Decidim
# This class is responsible of generating the Content-Security-Policy header
# for the application. It takes into account the organization's CSP settings
# and the additional settings defined in the initializer.
class ContentSecurityPolicy
SUPPORTED_POLICIES = %w(
child-src
connect-src
default-src
font-src
frame-src
img-src
manifest-src
media-src
object-src
prefetch-src
script-src
script-src-elem
script-src-attr
style-src-elem
style-src-attr
worker-src
base-uri
sandbox
form-action
frame-ancestors
navigate-to
report-uri
report-to
require-trusted-types-for
trusted-types
upgrade-insecure-requests
style-src
).freeze

def initialize(organization = nil, additional_policies = {})
@organization = organization
@policy = default_policy
@additional_policies = additional_policies
end

def output_policy
add_system_csp_directives
add_additional_policies
organization_csp_directives
append_development_directives

format_policies
end

def append_csp_directive(directive, value)
return if value.blank?

message = "Invalid Content Security Policy directive: #{directive}, supported directives: #{SUPPORTED_POLICIES.join(", ")}"
raise message unless SUPPORTED_POLICIES.include?(directive)

policy[directive] ||= []
policy[directive] << value
end

private

attr_reader :policy, :organization, :additional_policies

def append_development_directives
return unless Rails.env.development?

host = ::Webpacker.config.dev_server[:host]
port = ::Webpacker.config.dev_server[:port]

append_csp_directive("connect-src", "wss://#{host}:#{port}")
append_csp_directive("connect-src", "ws://#{host}:#{port}")
end

def format_policies
policy.map do |directive, values|
[directive, values.uniq.join(" ")].join(" ")
end.join("; ")
end

def append_multiple_csp_directives(directive, values)
values.each do |v|
append_csp_directive(directive, v)
end
end

def add_additional_policies
additional_policies.each do |directive, values|
next if values.blank?

values = values.split if values.is_a?(String)

raise "Invalid value for additional CSP policy #{directive}: #{values.inspect}" unless values.is_a?(Array)

append_multiple_csp_directives(directive, values)
end
end

def organization_csp_directives
return unless organization

organization.content_security_policy.each do |directive, value|
append_multiple_csp_directives(directive, value.split) if value.present?
end
end

def add_system_csp_directives
return unless Rails.configuration.action_controller.asset_host

%w(media-src img-src script-src style-src).each do |directive|
append_csp_directive(directive, Rails.configuration.action_controller.asset_host)
end
end

# rubocop:disable Lint/PercentStringArray
def default_policy
{
"default-src" => %w('self' 'unsafe-inline'),
"script-src" => %w('self' 'unsafe-inline' 'unsafe-eval'),
"style-src" => %w('self' 'unsafe-inline'),
"img-src" => %w('self' *.hereapi.com data:),
"font-src" => %w('self'),
"connect-src" => %w('self' *.hereapi.com *.jsdelivr.net),
"frame-src" => %w('self'),
"media-src" => %w('self')
}
end
# rubocop:enable Lint/PercentStringArray
end
end
8 changes: 8 additions & 0 deletions decidim-core/lib/decidim/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ module Decidim
autoload :Upgrade, "decidim/upgrade"
autoload :ParticipatorySpaceUser, "decidim/participatory_space_user"
autoload :ModerationTools, "decidim/moderation_tools"
autoload :ContentSecurityPolicy, "decidim/content_security_policy"

include ActiveSupport::Configurable
# Loads seeds from all engines.
Expand Down Expand Up @@ -523,6 +524,13 @@ def self.reset_all_column_information
%w(terms-of-service)
end

# List of additional content security policies to be appended to the default ones
# This is useful for adding custom CSPs for external services like Here Maps, YouTube, etc.
# Read more: https://docs.decidim.org/en/develop/configure/initializer/#_content-security-policy
config_accessor :content_security_policies_extra do
{}
end

# Public: Registers a global engine. This method is intended to be used
# by component engines that also offer unscoped functionality
#
Expand Down
1 change: 1 addition & 0 deletions decidim-core/lib/decidim/core/test/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def generate_localized_title
end
file_upload_settings { Decidim::OrganizationSettings.default(:upload) }
enable_participatory_space_filters { true }
content_security_policy { {} }

trait :secure_context do
host { "localhost" }
Expand Down
Loading

0 comments on commit 2a12210

Please sign in to comment.