Mitigate XSS Attacks with Content Security Policy

:bookmark: This guide explains how to use Content Security Policy (CSP) to mitigate Cross-Site Scripting (XSS) attacks in Discourse. It covers CSP basics, configuration, and best practices.

:person_raising_hand: Required user level: Administrator

Summary

Content Security Policy (CSP) is a crucial security feature in Discourse that helps protect against Cross-Site Scripting (XSS) and other injection attacks. This guide covers the basics of CSP, how it’s implemented in Discourse, and how to configure it for your site.

What is Content Security Policy?

Content Security Policy is an added layer of security that helps detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. CSP works by specifying which content sources are considered trusted, and instructing the browser to only execute or render resources from those trusted sources.

XSS remains one of the most common web vulnerabilities. By implementing CSP, Discourse allows scripts only from trusted sources to load and execute, significantly reducing the risk of XSS attacks.

Discourse’s CSP implementation

As of Discourse version 3.3.0.beta1, Discourse implements a ‘strict-dynamic’ CSP. This approach uses a single nonce- value and the strict-dynamic keyword in the script-src directive. All initial <script> tags in core and themes are automatically given the appropriate nonce= attribute.

The default policy includes the following directives:

  • script-src: Specifies valid sources for JavaScripts
  • worker-src: Specifies valid sources for ServiceWorker scripts
  • object-src: Blocks the execution of plugins (Flash, Java, etc.)
  • base-uri: Restricts the URLs for <base> elements
  • manifest-src: Restricts the URLs for web app manifests
  • frame-ancestors: Controls which sites can embed your Discourse instance in an iframe
  • upgrade-insecure-requests: Automatically upgrades HTTP requests to HTTPS (included when force_https is enabled)

Configuring CSP in Discourse

Available settings

  • content_security_policy: Enables or disables CSP (default: on)
  • content_security_policy_report_only: Enables CSP Report-Only mode (default: off)
  • content_security_policy_script_src: Allows you to extend the default script-src directive
  • content_security_policy_frame_ancestors: Enables the frame_ancestors directive (default: on)

How to enable CSP

  1. Navigate to your Admin panel
  2. Go to Security settings
  3. Find the content_security_policy setting and ensure it’s enabled

It’s recommended to start with CSP Report-Only mode to identify any potential issues before fully enabling CSP:

  1. Enable the content_security_policy_report_only setting
  2. Monitor your browser console for CSP violations
  3. Address any legitimate violations by extending the CSP as needed
  4. Once you’re confident there are no false positives, disable Report-Only mode and fully enable CSP

Extending the default CSP

If you need to allow additional script sources, you can extend the script-src directive using the content_security_policy_script_src setting. You can add:

  • Hash-sources
  • 'wasm-unsafe-eval'
  • 'unsafe-eval' (use with caution)

For example:

'sha256-QFlnYO2Ll+rgFRKkUmtyRublBc7KFNsbzF7BzoCqjgA=' 'unsafe-eval'

:warning: Be cautious when adding 'unsafe-eval' or other permissive directives, as they can reduce the effectiveness of CSP.

CSP and third-party integrations

When using third-party services like Google Tag Manager, Google Analytics, or advertising services, you may need to adjust your CSP settings. In most cases with Discourse version 3.3.0.beta1 or later, external scripts should work without additional configuration due to the ‘strict-dynamic’ CSP implementation.

If you encounter issues, you may need to:

  1. Identify the required script sources by monitoring your browser console
  2. Add the necessary sources to the content_security_policy_script_src setting
  3. For complex integrations like ad services which load external resources, you might need to enable cross-domain rendering (Example PR from discourse-adplugin that does this).

Best practices

  1. Start with CSP Report-Only mode to identify potential issues
  2. Gradually tighten your CSP as you resolve legitimate violations
  3. Regularly review your CSP settings and adjust as needed
  4. Be cautious when adding permissive directives like 'unsafe-eval' or 'wasm-unsafe-eval'
  5. Keep your Discourse instance updated to benefit from the latest CSP improvements

FAQs

Q: I’m seeing many CSP violation reports. Should I be concerned?
A: Many CSP violations are false positives, often caused by browser extensions or other unrelated scripts. Focus on addressing violations related to your site’s functionality.

Q: Can I use CSP with Google AdSense or other ad networks?
A: Yes, but you may need to use more permissive CSP settings. Start with Report-Only mode and adjust your settings based on the reported violations.

Q: How do I troubleshoot CSP issues?
A: Use your browser’s developer tools to monitor the console for CSP violation messages. These will help you identify which resources are being blocked and why.

Additional resources

Last edited by @kelv 2024-10-04T06:30:07Z

Check documentPerform check on document:
56 лайков
Adsense Not Working after Recent Discourse Update
Discourse 2.2.0.beta6 Release Notes
Adding statcounter code
How to install npm packages in custom themes/plugins
How to restart Discourse after server reboot?
Interactive SVG using <object>?
Video Upload to YouTube and Vimeo using Theme Component
Embed HTML5 player for MP3 file
2.5.0.beta5 breaks retort plugin
Should I load third-party libraries from vendor or cdn?
Word Cloud plugin
Embed widget within text in a topic
Discourse Intercom (Advanced)
Push custom events to Google Tag Manager and Analytics
Google Tag Manager and Discourse CSP (Content Security Policy)
Cookie Consent, GDPR, and Discourse
A strange question about google ad display in my site
How to pass a component setting as a value to an attribute?
Need help integrating code wrote on Edittext to the Discourse
Issue with Activate Account Page After Update to 3.4.0 (Blank Page)
"Unsafe JavaScript attempt to initiate navigation"
(Superseded) Experimenting with a 'strict-dynamic' Content Security Policy (CSP)
Can't get script tag to work in landing pages plugin due to content-security-policy
How to embed Razorpay subscription button with CSP restrictions
Any approved method for adding Javascript before body close?
Can I add a snippet to the header?
JS script is not loading
How do we fire scripts after topic HTML is rendered in DOM?
Iframe issue without URL
Where to place ad script?
We couldn't find the code on your site
Javascript not working in customised areas
Difficulties in correctly adding external JavaScript
Report Only CSP Violations
Nginx config in Discourse Docker?
EPN Smart Links
Discourse 2.2.0.beta9 Release Notes
"Refused to load the script" when adding adsense
Why Cookie Consent Doesn't Show Up?
[DigitalOcean] hostname having "www" in A records showing blank page
[DigitalOcean] hostname having "www" in A records showing blank page
Communities with embedded Twitter Feeds
How to add analytics and pixel scripts avoiding Content Security Policy (XSS)
When install html script facing issue?
Discourse 2.4.0.beta10 Release Notes
Add CSP sources to the plugin
IP does not redirect to domain, domain shows white page
DISCOURSE_CDN_URL causes content security policy violations?
Header content is missing due to CSP
How to insert something right after <head>?
How can I embed tracking JS into Discourse
How do I integrate ? cookiebot.com in meinen Forum?
Adding Cookie Consent Banner
Confused about remotely loaded javascript content
User input validation
Custom JS script in theme component not loading

I added a note about this to our public security.md file :tada:

13 лайков

As of this commit, we’ve turned off CSP violations reports by default because the vast majority of the reported violations are false positives.

To illustrate this, here is a screenshot of logs from a site running Discourse with CSP enabled and reporting enabled (filtered using “CSP Violation”):

All of the reported violations are not related to the site’s code:

  • violations with ‘minisrclink.cool’ or ‘proxdev.cool’ in the URL have nothing to do with Discourse, they’re likely coming from a browser extension
  • the Google Analytics violation reports are also not legitimate. They are triggered by Firefox in privacy mode, or Firefox with a privacy extension enabled (like DuckDuckGo Privacy Essentials).
  • Violations with ‘inline’, ‘data’ or ‘about’ are triggered by extensions as well. It’s not shown in the screenshot above, but these violations have some more details in the env tab of the log. In there, under script-sample, some of these violations had code like BlockAdBlock or window.klTabId_kis or AG_onLoad, which come from the AdBlock, Kaspersky, and AdGuard extensions, respectively. (I found this repo: CSP-useful/csp-wtf/README.md at master · nico3333fr/CSP-useful · GitHub very useful in helping explain some of these reports.) Some of these violations will have safari-extension or user-script in the source-file variable (again, in env), so that points to Safari extensions as the culprit for the violation.

In other words, there’s a lot of noise in CSP violation reports, so it’s not useful to log them at all times. They might be helpful while you are configuring CSP, but the reporting should be off during the normal operation of a site.

A few final notes: if you site is using a tag manager (like Google Tag Manager or Segment) you need to load the site in your browser, and carefully examine the violations in the console. These tools load third-party scripts from third-party domains and/or inline scripts so you need to carefully whitelist each of them using the source URL or the hash of the inline script (Chrome usefully includes the hash of inline scripts in the console error statement).

If your site uses an advertising service (like Google Ad Manager, Adsense, etc.) you probably will have to use a very permissive policy:

In the screenshot above, the policy allows any script from a https: source and any inline script. (In the future, this might be replaced by the strict-dynamic keyword, but as of this writing, strict-dynamic isn’t supported by Safari or Edge.)

24 лайка

8 posts were split to a new topic: Protocol-less CDN URLs are problematic

Please note that Safari doesn’t understand some parts of CSP, and this is normal:

You can safely ignore the CSP errors in Safari, you’ll see those on all sites, it just means Safari doesn’t understand worker-src and report-sample .

I guess we need to wait for Safari to be updated?

12 лайков

У меня возникли проблемы с настройкой. Какую рекомендацию выбрать?

Мой форум: forum.meuxbox.com.br

Ссылка: https://meta.discourse.org/t/white-blank-advertisement/140098/10?u=eduardo_braga

2 лайка

Это зависит от того, какие URL запрашивают ваши объявления. Вы можете посмотреть их в консоли вашего браузера.

Также см. соответствующий раздел из оригинального поста:

4 лайка

https://meta.discourse.org/t/white-blank-advertisement/140098/3?u=eduardo_braga

это решает проблему ошибки

6 лайков

Не могли бы вы добавить Feature-Policy?

Я использую его уже более года. (хост nginx)

add_header Feature-Policy “geolocation ‘none’; midi ‘none’; notifications ‘self’; push ‘none’; sync-xhr ‘none’; microphone ‘none’; camera ‘none’; magnetometer ‘none’; gyroscope ‘none’; speaker ‘none’; vibrate ‘none’; fullscreen ‘none’; payment ‘none’;”;


Имеет ли смысл добавить следующее в заголовок Content-Security-Policy? Это то, что я успешно использую (добавлено хостом nginx поверх встроенного CSP Discourse):

default-src 'none'
style-src 'self' domain 'unsafe-inline'
img-src https://*.domain.org data: blob: 'unsafe-inline'
font-src 'self' domain
connect-src 'self' domain
manifest-src 'self' domain
3 лайка

Учитывая, что спецификация всё ещё находится в стадии черновика, я не ожидаю, что мы будем её реализовывать на данном этапе. Даже на сайте Mozilla, который вы привели, сказано:

Заголовок Feature-Policy всё ещё находится в экспериментальной стадии и может быть изменён в любой момент. Будьте осторожны при реализации Feature Policy на вашем сайте.

8 лайков

vibrate 'self' — лайки на Android вызывают слабую короткую вибрацию.

10 лайков

Я только что установил Discourse 2.6.0.beta1. Нужно ли мне перенастраивать его? Спасибо.

1 лайк

Нет, по умолчанию это включено. Изменять конфигурацию нужно только в том случае, если вы видите заблокированные внешние ресурсы и хотите, чтобы они работали.

4 лайка

Я работаю на версии 2.6.0 beta 2.

В форумах я использую следующие сервисы:

  • Google Tag Manager
  • Google Ad Manager
  • Google AdSense

В настоящее время я использую режим отчётов CSP, пока пытаюсь решить все открытые вопросы.

Вот мои настройки CSP:

При текущих настройках я всё ещё получаю МНОГО ошибок CSP. Некоторые из них, кажется, можно игнорировать. Однако эта одна меня ставит в тупик, так как домен уже объявлен в настройках CSP.

Не упустил ли я что-то?

CSP Violation: 'https://www.googletagmanager.com/gtm.js?id=GTM-T9ZW6PR'

Трассировка стека:

/var/www/discourse/app/controllers/csp_reports_controller.rb:9:in `create'
actionpack-6.0.3.2/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
actionpack-6.0.3.2/lib/abstract_controller/base.rb:195:in `process_action'
actionpack-6.0.3.2/lib/action_controller/metal/rendering.rb:30:in `process_action'
actionpack-6.0.3.2/lib/abstract_controller/callbacks.rb:42:in `block in process_action'
activesupport-6.0.3.2/lib/active_support/callbacks.rb:112:in `block in run_callbacks'
/var/www/discourse/app/controllers/application_controller.rb:340:in `block in with_resolved_locale'
i18n-1.8.5/lib/i18n.rb:313:in `with_locale'
/var/www/discourse/app/controllers/application_controller.rb:340:in `with_resolved_locale'
activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
activesupport-6.0.3.2/lib/active_support/callbacks.rb:139:in `run_callbacks'
actionpack-6.0.3.2/lib/abstract_controller/callbacks.rb:41:in `process_action'
actionpack-6.0.3.2/lib/action_controller/metal/rescue.rb:22:in `process_action'
actionpack-6.0.3.2/lib/action_controller/metal/instrumentation.rb:33:in `block in process_action'
activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `block in instrument'
activesupport-6.0.3.2/lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `instrument'
actionpack-6.0.3.2/lib/action_controller/metal/instrumentation.rb:32:in `process_action'
actionpack-6.0.3.2/lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
activerecord-6.0.3.2/lib/active_record/railties/controller_runtime.rb:27:in `process_action'
actionpack-6.0.3.2/lib/abstract_controller/base.rb:136:in `process'
actionview-6.0.3.2/lib/action_view/rendering.rb:39:in `process'
rack-mini-profiler-2.0.4/lib/mini_profiler/profiling_methods.rb:78:in `block in profile_method'
actionpack-6.0.3.2/lib/action_controller/metal.rb:190:in `dispatch'
actionpack-6.0.3.2/lib/action_controller/metal.rb:254:in `dispatch'
actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:50:in `dispatch'
actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:33:in `serve'
actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:49:in `block in serve'
actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:32:in `each'
actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:32:in `serve'
actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:834:in `call'
/var/www/discourse/lib/middleware/omniauth_bypass_middleware.rb:68:in `call'
rack-2.2.3/lib/rack/tempfile_reaper.rb:15:in `call'
rack-2.2.3/lib/rack/conditional_get.rb:40:in `call'
rack-2.2.3/lib/rack/head.rb:12:in `call'
/var/www/discourse/lib/content_security_policy/middleware.rb:12:in `call'
/var/www/discourse/lib/middleware/anonymous_cache.rb:336:in `call'
rack-2.2.3/lib/rack/session/abstract/id.rb:266:in `context'
rack-2.2.3/lib/rack/session/abstract/id.rb:260:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/cookies.rb:648:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'
activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'
actionpack-6.0.3.2/lib/action_dispatch/middleware/callbacks.rb:26:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/debug_exceptions.rb:32:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
logster-2.9.3/lib/logster/middleware/reporter.rb:43:in `call'
railties-6.0.3.2/lib/rails/rack/logger.rb:37:in `call_app'
railties-6.0.3.2/lib/rails/rack/logger.rb:28:in `call'
/var/www/discourse/config/initializers/100-quiet_logger.rb:19:in `call'
/var/www/discourse/config/initializers/100-silence_logger.rb:31:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/request_id.rb:27:in `call'
/var/www/discourse/lib/middleware/enforce_hostname.rb:22:in `call'
rack-2.2.3/lib/rack/method_override.rb:24:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/executor.rb:14:in `call'
rack-2.2.3/lib/rack/sendfile.rb:110:in `call'
actionpack-6.0.3.2/lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
rack-mini-profiler-2.0.4/lib/mini_profiler/profiler.rb:200:in `call'
message_bus-3.3.1/lib/message_bus/rack/middleware.rb:61:in `call'
/var/www/discourse/lib/middleware/request_tracker.rb:176:in `call'
railties-6.0.3.2/lib/rails/engine.rb:527:in `call'
railties-6.0.3.2/lib/rails/railtie.rb:190:in `public_send'
railties-6.0.3.2/lib/rails/railtie.rb:190:in `method_missing'
rack-2.2.3/lib/rack/urlmap.rb:74:in `block in call'
rack-2.2.3/lib/rack/urlmap.rb:58:in `each'
rack-2.2.3/lib/rack/urlmap.rb:58:in `call'
unicorn-5.6.0/lib/unicorn/http_server.rb:632:in `process_client'
unicorn-5.6.0/lib/unicorn/http_server.rb:728:in `worker_loop'
unicorn-5.6.0/lib/unicorn/http_server.rb:548:in `spawn_missing_workers'
unicorn-5.6.0/lib/unicorn/http_server.rb:144:in `start'
unicorn-5.6.0/bin/unicorn:128:in `<top (required)>'
/var/www/discourse/vendor/bundle/ruby/2.6.0/bin/unicorn:23:in `load'
/var/www/discourse/vendor/bundle/ruby/2.6.0/bin/unicorn:23:in `<main>'

Окружение 1:


hostname	forums-web-only
process_id	27127
application_version	f2e14a3946b020ace5a368614f0da198cd17aa32
HTTP_HOST	forums.paddling.com
REQUEST_URI	/csp_reports
REQUEST_METHOD	POST
HTTP_USER_AGENT	Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0
HTTP_ACCEPT	*/*
HTTP_X_FORWARDED_FOR	74.76.45.218
HTTP_X_REAL_IP	74.76.45.218
time	1:44 pm

Окружение 2:


hostname	forums-web-only
process_id	27161
application_version	f2e14a3946b020ace5a368614f0da198cd17aa32
HTTP_HOST	forums.paddling.com
REQUEST_URI	/csp_reports
REQUEST_METHOD	POST
HTTP_USER_AGENT	Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0
HTTP_ACCEPT	*/*
HTTP_X_FORWARDED_FOR	66.58.144.146
HTTP_X_REAL_IP	66.58.144.146
time	1:39 pm

Окружение 3:

hostname	forums-web-only
process_id	27111
application_version	f2e14a3946b020ace5a368614f0da198cd17aa32
HTTP_HOST	forums.paddling.com
REQUEST_URI	/csp_reports
REQUEST_METHOD	POST
HTTP_USER_AGENT	Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
HTTP_ACCEPT	*/*
HTTP_REFERER	https://forums.paddling.com/t/need-advice-loading-a-kayak-onto-j-rac/60058
HTTP_X_FORWARDED_FOR	174.83.24.11
HTTP_X_REAL_IP	174.83.24.11
time	12:16 pm
2 лайка

В отчётах CSP много ложных срабатываний. Подробнее см. мой ответ выше.

Вам не нужны большинство правил, которые вы установили на скриншоте; достаточно https: и unsafe-inline — они разрешают все скрипты, начинающиеся с https, и все встроенные скрипты. Попробуйте очистить настройки источников CSP и включить CSP (без отправки отчётов), всё должно работать.

8 лайков

Привет, просто уточняю: директива Content-Security-Policy: frame-ancestors ‘none’ — это та, которая «будет включена в будущих обновлениях», как упоминал автор темы?

Возможно ли как-то добавить эту директиву или мне просто подождать и не беспокоиться об этом? Я просто проводил упражнение по усилению защиты, и это был единственный открытый пункт/рекомендация от того онлайн-инструмента безопасности. Это, наоборот, внушает мне большое доверие к платформе, отличная работа, ребята!

5 лайков

Я не уверен — что ты думаешь, @xrav3nz?

4 лайка

Я бы сказал, пока не стоит об этом беспокоиться.

frame-ancestors аналогичен заголовку X-Frame-Options, который уже принудительно применяется в Discourse/Rails. В настоящее время заголовок установлен в значение sameorigin — примерно то же самое, что и опция self в директиве CSP.

По моему мнению, внедрение frame-ancestors сейчас не принесёт особой пользы, если только нам не потребуется поддержка добавления в белый список конкретных доменов, отличных от self.

8 лайков

Я согласен с этим.

Попытка устранить каждую возможную «проблему», выявленную сканерами уязвимостей, может в итоге нарушить работу важных компонентов, при этом соотношение риска и выгоды будет крайне низким.

Существует множество «теоретических уязвимостей», которые либо редко эксплуатируются, либо могут быть использованы только в очень специфических сценариях. К сожалению, на мой взгляд, такие потенциальные уязвимости никогда не использовались для атак на сайты Discourse. Поэтому я не рекомендую «исправлять» то, что является уязвимостью лишь «в теории» и не привело к каким-либо значимым нарушениям.

Это моё мнение как специалиста по кибербезопасности с многолетним опытом. Я с радостью обсудю основы управления рисками в области кибербезопасности, если кому-то это интересно.

5 лайков

Мы только что добавили поддержку директивы CSP frame-ancestors. Пока она отключена по умолчанию и доступна через настройку сайта content security policy frame ancestors. Вы можете добавлять домены в список, как обычно, через /admin/customize/embedding.

В следующем цикле релизов эта директива будет включена по умолчанию.

7 лайков