JS `Content Security Policy (CSP)`:防范 XSS 与内容注入攻击

各位观众老爷们,大家好! 欢迎来到今天的安全小课堂,我是你们的老朋友,bug终结者。 今天咱们聊点刺激的,聊聊怎么像个老中医一样,把XSS这种烦人的“皮肤病”扼杀在摇篮里,靠的呢,就是我们今天要讲的“Content Security Policy (CSP)”,中文名叫“内容安全策略”。 听着是不是很高级?别怕,其实就是给你的网站穿上一件定制的“安全马甲”。

一、 啥是XSS?为啥需要CSP?

先来说说XSS,这玩意儿全称“Cross-Site Scripting”,翻译过来就是“跨站脚本攻击”。 听着玄乎,其实就是坏人想办法往你的网站里塞点恶意代码,比如偷偷摸摸地盗取用户的cookie,或者更过分地篡改页面内容,甚至直接跳转到钓鱼网站。

想象一下:你辛辛苦苦搭建的网站,本来是卖萌的,结果被坏人塞了一段代码,变成了诈骗犯,这谁能忍?

那CSP是干啥的呢? 简单来说,CSP就是告诉浏览器:“嘿,哥们儿,我这个网站只能加载来自这些地方的资源,其他的统统给我拒!绝!”。 就像海关一样,严格审查进出境的“货物”(资源),把那些可疑的“走私品”(恶意脚本)挡在门外。

所以,CSP就是专门用来对付XSS这种“病毒”的“疫苗”。

二、 CSP的基本语法:指令和来源

CSP的核心在于配置HTTP响应头 Content-Security-Policy。 这个头部里包含一系列的“指令”,每个指令后面跟着允许的“来源”。 就像你在跟浏览器下命令,告诉它哪些来源的资源可以加载。

比如,最简单的CSP:

Content-Security-Policy: default-src 'self'

这行代码的意思是:“默认情况下,只允许加载来自我自己的服务器('self')的资源”。 啥叫“默认情况下”? 就是说,如果你没有专门指定图片、脚本、样式表等资源的来源,那就都按这个默认的来。

再来一个稍微复杂点的:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data:; style-src 'self' https://fonts.googleapis.com

这行代码的意思是:

  • default-src 'self': 默认情况下,只允许加载来自我自己的服务器的资源。
  • script-src 'self' https://cdn.example.com: 允许加载来自我自己的服务器和 https://cdn.example.com 的脚本。 注意,这里指定了脚本的来源,所以默认规则对脚本无效。
  • img-src 'self' data:: 允许加载来自我自己的服务器的图片,以及使用 data: URI 内嵌的图片。 data: URI 是一种把图片直接编码到HTML里的方式,比如 <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w+r8+vv///+8gL+E/EAgQAOgXADAw==" alt="Red dot" />
  • style-src 'self' https://fonts.googleapis.com: 允许加载来自我自己的服务器和 https://fonts.googleapis.com 的样式表。

看到了吧? CSP的语法就是这么简单粗暴,就是一堆指令和来源的组合。

三、 常见的CSP指令

下面咱们来详细说说一些常用的CSP指令:

指令 作用 例子
default-src 定义所有类型资源的默认来源。如果其他指令没有明确指定来源,就使用这个默认值。 default-src 'self'
script-src 定义JavaScript脚本的有效来源。 script-src 'self' https://cdn.example.com
style-src 定义CSS样式表的有效来源。 style-src 'self' https://fonts.googleapis.com
img-src 定义图片的有效来源。 img-src 'self' data: https://images.example.com
connect-src 定义允许XMLHttpRequest (AJAX), WebSocket 和 EventSource 连接的来源。 connect-src 'self' https://api.example.com
font-src 定义字体文件的有效来源。 font-src 'self' https://fonts.example.com
media-src 定义音视频文件的有效来源。 media-src 'self' https://media.example.com
object-src 定义<object>, <embed><applet> 元素的有效来源。(一般不建议使用这些元素) object-src 'none' (禁用所有object, embed和applet)
frame-src 定义<frame><iframe> 元素的有效来源。 frame-src 'self' https://youtube.com
child-src frame-src 的替代品,用于 web workers 和嵌入的 frame 内容。 (建议使用frame-src代替,因为 child-src 已经过时) child-src 'self'
manifest-src 定义 manifest 文件的有效来源。 manifest-src 'self'
form-action 定义表单提交的有效目标地址。 form-action 'self' https://secure.example.com
base-uri 定义 <base> 元素的有效来源。 base-uri 'self'
plugin-types 限制浏览器可以加载的插件类型。 (现在插件用的越来越少了,这个指令也用的比较少) plugin-types application/pdf application/x-shockwave-flash
sandbox 为请求的资源启用沙箱。 类似于 <iframe> 标签的 sandbox 属性。 sandbox allow-forms allow-scripts
report-uri (已弃用,推荐使用 report-to) 指定一个URL,当CSP策略被违反时,浏览器会向该URL发送一个报告。 report-uri /csp-report
report-to 指定一个或多个报告组,浏览器会将CSP违规报告发送到这些组定义的端点。 需要配合 Report-To HTTP头部一起使用。 report-to csp-endpoint
worker-src 指定 worker 脚本的有效来源。 worker-src 'self'
upgrade-insecure-requests 指示浏览器自动将页面上所有不安全的URL(HTTP)升级为安全的URL(HTTPS)。 对于 HTTPS 站点来说,这是一个很好的做法。 upgrade-insecure-requests
require-trusted-types-for 强制使用 Trusted Types API 来防止 DOM 型 XSS 攻击。 Trusted Types 是一种更高级的 CSP 技术,用于严格控制 DOM 操作。 这部分内容比较复杂,我们后面单独讲。 require-trusted-types-for 'script'
trusted-types 配置 Trusted Types 策略。 同样,这部分内容我们后面单独讲。 trusted-types default allow-duplicates

四、 CSP的来源值:’self’、’none’、’unsafe-inline’、’unsafe-eval’ 和通配符

上面表格里的 “来源”,可不是随便填的,它有一些特殊的取值:

  • 'self': 表示与文档来源相同的来源。 简单来说,就是你自己的服务器。 注意: http://example.comhttps://example.com 是不同的来源,即使它们指向同一个服务器。
  • 'none': 表示不允许加载任何来源的资源。 相当于彻底禁用了某种类型的资源。 比如 script-src 'none', 意味着你的页面里不能执行任何脚本,包括内联脚本和外部脚本。
  • 'unsafe-inline': 允许使用内联的 JavaScript 和 CSS。 比如 <script>alert('hello')</script><style>body { background: red; }</style>强烈不建议使用! 因为这会给XSS攻击留下可乘之机。 如果一定要用内联脚本或样式,可以考虑使用 noncehash
  • 'unsafe-eval': 允许使用 eval() 函数和其他类似的方法,比如 new Function()同样不建议使用! 因为这些方法可以执行任意的字符串作为代码,也会带来安全风险。
  • 'unsafe-hashes': 允许特定的内联事件处理程序(例如 onclick)。这个指令需要配合哈希值使用,以精确控制允许哪些内联事件处理程序。 例如:script-src 'unsafe-hashes' 'sha256-YOUR_HASH_VALUE'。 使用这个指令比 'unsafe-inline' 更安全,因为它只允许特定的内联事件处理程序,而不是所有。
  • 'strict-dynamic': 允许通过信任的脚本添加的脚本自动获得信任。 这个指令通常和 noncehash 一起使用,用于动态加载脚本的场景。 具体用法比较复杂,我们后面单独讲。
  • data:: 允许使用 data: URI。 主要用于内嵌图片和其他数据。 使用 data: URI 要小心,因为它可以被用来绕过某些安全限制。
  • mediastream:: 允许使用 mediastream: URI。 用于访问用户的摄像头和麦克风。
  • *通配符 ``: 允许来自任何来源的资源。 非常不建议使用!** 这相当于放弃了CSP的保护,让你的网站暴露在XSS攻击的风险之下。
  • 主机名: 比如 https://example.com。 允许加载来自指定主机名的资源。 可以包含端口号,比如 https://example.com:8080
  • 域名通配符: 比如 *.example.com。 允许加载来自指定域名及其所有子域名的资源。

五、 如何设置CSP:HTTP头部 vs. <meta> 标签

设置CSP有两种方式:

  1. HTTP头部: 这是最推荐的方式。 通过服务器配置来设置 Content-Security-Policy 头部。 比如,在 Apache 的 .htaccess 文件里添加:

    Header set Content-Security-Policy "default-src 'self'"

    或者在 Nginx 的配置文件里添加:

    add_header Content-Security-Policy "default-src 'self'";

    在 Node.js 里,可以使用 helmet 中间件来方便地设置CSP头部:

    const helmet = require('helmet');
    const express = require('express');
    const app = express();
    
    app.use(
      helmet.contentSecurityPolicy({
        directives: {
          defaultSrc: ["'self'"],
        },
      })
    );
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });
  2. <meta> 标签: 可以在HTML文档的 <head> 标签里使用 <meta> 标签来设置CSP。

    <!DOCTYPE html>
    <html>
    <head>
      <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
      <title>My Website</title>
    </head>
    <body>
      <h1>Hello, world!</h1>
    </body>
    </html>

    注意: 使用 <meta> 标签设置CSP有一些限制,比如不能使用 report-urisandbox 指令。 所以,强烈建议使用HTTP头部来设置CSP。

六、 CSP的“报告模式”:Content-Security-Policy-Report-Only

CSP还有一个“报告模式”,通过设置 Content-Security-Policy-Report-Only HTTP头部来启用。 在这个模式下,浏览器不会阻止任何资源加载,但是会把违反CSP策略的事件报告到指定的URL。

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

或者使用 report-to 指令:

Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":31536000,"endpoints":[{"url":"/csp-report"}]}

“报告模式”非常有用,可以在不影响用户体验的情况下,测试和调整你的CSP策略。 你可以先在“报告模式”下运行一段时间,收集违规报告,然后根据报告来修改你的CSP策略,最后再切换到“强制模式”。

七、 CSP的“nonce”和“hash”:更安全的内联脚本和样式

上面我们说过,'unsafe-inline' 是非常不安全的,应该尽量避免使用。 但是,有时候我们确实需要使用内联脚本或样式,比如为了性能优化,或者为了实现一些特殊的效果。 这时候,就可以使用 noncehash 来更安全地使用内联脚本和样式。

  • Nonce: Nonce是一个随机字符串,每次页面加载时都会生成一个新的nonce。 你需要在CSP策略里指定允许的nonce值,然后在内联脚本和样式的标签里添加 nonce 属性,并设置相同的值。

    例如:

    Content-Security-Policy: script-src 'self' 'nonce-EDNnf03nceIOfmxp3rv9wrjhr3Nonc3Eg=='
    <script nonce="EDNnf03nceIOfmxp3rv9wrjhr3Nonc3Eg==">
      alert('Hello, world!');
    </script>

    只有 nonce 属性值和CSP策略里指定的nonce值相同的内联脚本才能执行。 这样可以防止攻击者注入恶意的内联脚本。

  • Hash: Hash是内联脚本或样式的SHA256、SHA384或SHA512哈希值。 你需要在CSP策略里指定允许的哈希值。

    例如:

    Content-Security-Policy: script-src 'self' 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob/T4Y='
    <script>
      alert('Hello, world!');
    </script>

    只有哈希值和CSP策略里指定的哈希值相同的内联脚本才能执行。 这样可以确保只有你信任的内联脚本才能执行。

    注意: 使用hash的时候,脚本内容不能有任何修改,包括空格和换行符。 所以,hash更适合于静态的、不会改变的内联脚本。

八、 CSP和Trusted Types:更高级的DOM XSS防御

CSP可以有效地防止注入型的XSS攻击,但是对于DOM型的XSS攻击,效果就比较有限了。 DOM型的XSS攻击是指攻击者通过修改页面的DOM结构来执行恶意代码。

为了更有效地防御DOM型的XSS攻击,可以使用Trusted Types API。 Trusted Types是一种更高级的CSP技术,用于严格控制DOM操作。

Trusted Types的核心思想是: 所有插入到DOM里的数据都必须是“可信任的”类型。 浏览器会检查所有DOM操作,如果插入的数据不是Trusted Types,就会阻止操作。

要使用Trusted Types,首先需要在CSP策略里启用 require-trusted-types-for 指令:

Content-Security-Policy: require-trusted-types-for 'script'

这个指令的意思是: 所有通过脚本插入到DOM里的数据都必须是Trusted Types。

然后,你需要创建一个或多个Trusted Types策略,来定义哪些类型的数据是“可信任的”。

例如:

if (window.trustedTypes && trustedTypes.createPolicy) {
  trustedTypes.createPolicy('default', {
    createHTML: (string) => string.replace(/</g, '&lt;'), // 避免HTML注入
    createScriptURL: (string) => {
      if (string.startsWith('https://')) {
        return string; // 只允许HTTPS的URL
      }
      throw new Error('Untrusted URL: ' + string);
    },
    createScript: (string) => string, // 允许任意脚本
  });
}

这个例子创建了一个名为 default 的Trusted Types策略,它定义了三个方法:

  • createHTML: 用于创建可信任的HTML字符串。 这个方法会对输入的字符串进行HTML转义,避免HTML注入。
  • createScriptURL: 用于创建可信任的脚本URL。 这个方法会检查输入的URL是否以 https:// 开头,如果不是,就抛出一个错误。
  • createScript: 用于创建可信任的脚本。 这个方法不做任何处理,直接返回输入的字符串。 注意: 在生产环境里,应该对这个方法进行更严格的限制,比如只允许加载来自白名单的脚本。

创建了Trusted Types策略之后,你就可以使用它来创建可信任的数据,然后插入到DOM里。

例如:

const policy = trustedTypes.getDefaultPolicy();
const trustedHTML = policy.createHTML('<p>Hello, world!</p>');
document.body.innerHTML = trustedHTML;

const trustedScriptURL = policy.createScriptURL('https://example.com/script.js');
const script = document.createElement('script');
script.src = trustedScriptURL;
document.body.appendChild(script);

如果尝试插入未经Trusted Types处理的数据,浏览器就会阻止操作,并抛出一个错误。

Trusted Types是一种非常强大的DOM XSS防御技术,但是也比较复杂,需要一定的学习成本。

九、 CSP的最佳实践

最后,我们来总结一下CSP的最佳实践:

  1. 使用HTTP头部设置CSP。 避免使用 <meta> 标签。
  2. 从最严格的策略开始,逐步放宽。 先设置一个非常严格的策略,只允许加载来自自己的服务器的资源,然后逐步添加其他来源。
  3. 使用“报告模式”测试你的CSP策略。 在不影响用户体验的情况下,收集违规报告,并根据报告来修改你的CSP策略。
  4. 避免使用 'unsafe-inline''unsafe-eval' 如果一定要使用内联脚本或样式,可以使用 noncehash
  5. *不要使用通配符 ``。** 这会降低CSP的安全性。
  6. 定期审查和更新你的CSP策略。 随着你的网站的变化,你的CSP策略也需要更新。
  7. 使用 Trusted Types API 来防御DOM型的XSS攻击。
  8. 使用CSP兼容性检查工具。 比如 https://csp-evaluator.withgoogle.com/ 可以帮助你检查你的CSP策略是否存在问题。

十、 总结

好了,今天的CSP小课堂就到这里了。 希望大家通过今天的学习,能够对CSP有一个更深入的了解,并能够在自己的网站上正确地配置CSP,保护用户的安全。

记住: 安全无小事! 只有做好每一个细节,才能让你的网站更加安全可靠。

谢谢大家! 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注