使用内容安全策略缓解 XSS 攻击

:bookmark: 本指南介绍如何使用内容安全策略(CSP)来缓解 Discourse 中的跨站脚本(XSS)攻击。内容涵盖 CSP 基础、配置方法及最佳实践。

:person_raising_hand: 所需用户级别:管理员

摘要

内容安全策略(CSP)是 Discourse 中一项关键的安全功能,有助于防范跨站脚本(XSS)及其他注入攻击。本指南将介绍 CSP 的基础知识、其在 Discourse 中的实现方式,以及如何为您的站点进行配置。

什么是内容安全策略?

内容安全策略是一种额外的安全层,有助于检测并缓解某些类型的攻击,包括跨站脚本(XSS)和数据注入攻击。CSP 通过指定哪些内容来源被视为可信,并指示浏览器仅执行或渲染来自这些可信来源的资源来发挥作用。

XSS 仍然是最常见的 Web 漏洞之一。通过实施 CSP,Discourse 仅允许来自可信来源的脚本加载和执行,从而显著降低 XSS 攻击的风险。

Discourse 的 CSP 实现

从 Discourse 3.3.0.beta1 版本开始,Discourse 实现了“严格动态(strict-dynamic)”CSP。该方法在 script-src 指令中使用单个 nonce- 值和 strict-dynamic 关键字。核心和主题中的所有初始 <script> 标签都会自动添加适当的 nonce= 属性。

默认策略包含以下指令:

  • script-src:指定 JavaScript 的有效来源
  • worker-src:指定 ServiceWorker 脚本的有效来源
  • object-src:阻止插件(Flash、Java 等)的执行
  • base-uri:限制 <base> 元素的 URL
  • manifest-src:限制 Web 应用清单的 URL
  • frame-ancestors:控制哪些站点可以将您的 Discourse 实例嵌入到 iframe 中
  • upgrade-insecure-requests:自动将 HTTP 请求升级为 HTTPS(在启用 force_https 时包含)

在 Discourse 中配置 CSP

可用设置

  • content_security_policy:启用或禁用 CSP(默认:开启)
  • content_security_policy_report_only:启用 CSP 报告模式(默认:关闭)
  • content_security_policy_script_src:允许您扩展默认的 script-src 指令
  • content_security_policy_frame_ancestors:启用 frame_ancestors 指令(默认:开启)

如何启用 CSP

  1. 导航至您的管理面板
  2. 进入安全设置
  3. 找到 content_security_policy 设置并确保其已启用

建议先启用 CSP 报告模式以识别任何潜在问题,然后再完全启用 CSP:

  1. 启用 content_security_policy_report_only 设置
  2. 监控浏览器控制台中的 CSP 违规报告
  3. 根据需要扩展 CSP 以解决任何合法的违规
  4. 一旦确认没有误报,请禁用报告模式并完全启用 CSP

扩展默认 CSP

如果您需要允许额外的脚本来源,可以使用 content_security_policy_script_src 设置扩展 script-src 指令。您可以添加:

  • 哈希来源
  • 'wasm-unsafe-eval'
  • 'unsafe-eval'(请谨慎使用)

例如:

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

:warning: 添加 'unsafe-eval' 或其他宽松指令时请务必谨慎,因为它们可能会降低 CSP 的有效性。

CSP 与第三方集成

当使用 Google Tag Manager、Google Analytics 或广告服务等第三方服务时,您可能需要调整 CSP 设置。在大多数情况下,使用 Discourse 3.3.0.beta1 或更高版本时,由于实施了“严格动态”CSP,外部脚本无需额外配置即可正常工作。

如果您遇到问题,可能需要:

  1. 通过监控浏览器控制台识别所需的脚本来源
  2. 将必要的来源添加到 content_security_policy_script_src 设置中
  3. 对于像广告服务这样加载外部资源的复杂集成,您可能需要启用跨域渲染(参考 discourse-adplugin 的相关 PR 示例)。

最佳实践

  1. 从 CSP 报告模式开始,以识别潜在问题
  2. 在解决合法违规后,逐步收紧 CSP
  3. 定期审查 CSP 设置并根据需要进行调整
  4. 添加 'unsafe-eval''wasm-unsafe-eval' 等宽松指令时请谨慎
  5. 保持 Discourse 实例更新,以利用最新的 CSP 改进

常见问题解答

问:我看到了很多 CSP 违规报告。我应该担心吗?
答:许多 CSP 违规是误报,通常由浏览器扩展或其他无关脚本引起。请重点关注与站点功能相关的违规。

问:我可以在 CSP 中使用 Google AdSense 或其他广告网络吗?
答:可以,但您可能需要使用更宽松的 CSP 设置。建议从报告模式开始,并根据报告的违规情况调整设置。

问:如何排查 CSP 问题?
答:使用浏览器的开发者工具监控控制台中的 CSP 违规消息。这将帮助您识别哪些资源被阻止以及原因。

其他资源

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 添加,在 Discourse 内置的 CSP 之上):

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 违规:'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,大致相当于 CSP 指令中的 self 选项。

依我之见,除非我们需要支持将 self 以外的特定域名加入白名单,否则现在实施 frame-ancestors 不会带来太大收益。

8 个赞

我同意这一观点。

一味追逐所有可能的漏洞扫描器“问题”,可能会破坏重要功能,而风险收益比却极低。

存在许多“理论上的漏洞”,它们很少被利用,或者只能在非常特定的场景下被利用。据我所知,这类潜在漏洞从未在 Discourse 站点上被实际利用过;因此,我建议不要“修复”那些仅在“理论上”构成漏洞、且从未导致任何实质性安全事件的问题。

作为一名拥有数十年经验的网络安全专业人士,以上是我的个人看法。如果有人感兴趣,我很乐意讨论网络安全风险管理的基础知识。

5 个赞

我们刚刚实现了对 CSP frame-ancestors 指令的支持。目前该功能默认处于禁用状态,位于 content security policy frame ancestors 站点设置之后。您可以像往常一样,通过 /admin/customize/embedding 向列表中添加域名。

该指令将在下一个发布周期中默认启用。

7 个赞