各位观众老爷们,大家好! 欢迎来到今天的安全小课堂,我是你们的老朋友,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.com
和https://example.com
是不同的来源,即使它们指向同一个服务器。'none'
: 表示不允许加载任何来源的资源。 相当于彻底禁用了某种类型的资源。 比如script-src 'none'
, 意味着你的页面里不能执行任何脚本,包括内联脚本和外部脚本。'unsafe-inline'
: 允许使用内联的 JavaScript 和 CSS。 比如<script>alert('hello')</script>
和<style>body { background: red; }</style>
。 强烈不建议使用! 因为这会给XSS攻击留下可乘之机。 如果一定要用内联脚本或样式,可以考虑使用nonce
或hash
。'unsafe-eval'
: 允许使用eval()
函数和其他类似的方法,比如new Function()
。 同样不建议使用! 因为这些方法可以执行任意的字符串作为代码,也会带来安全风险。'unsafe-hashes'
: 允许特定的内联事件处理程序(例如onclick
)。这个指令需要配合哈希值使用,以精确控制允许哪些内联事件处理程序。 例如:script-src 'unsafe-hashes' 'sha256-YOUR_HASH_VALUE'
。 使用这个指令比'unsafe-inline'
更安全,因为它只允许特定的内联事件处理程序,而不是所有。'strict-dynamic'
: 允许通过信任的脚本添加的脚本自动获得信任。 这个指令通常和nonce
或hash
一起使用,用于动态加载脚本的场景。 具体用法比较复杂,我们后面单独讲。data:
: 允许使用data:
URI。 主要用于内嵌图片和其他数据。 使用data:
URI 要小心,因为它可以被用来绕过某些安全限制。mediastream:
: 允许使用mediastream:
URI。 用于访问用户的摄像头和麦克风。- *通配符 ``: 允许来自任何来源的资源。 非常不建议使用!** 这相当于放弃了CSP的保护,让你的网站暴露在XSS攻击的风险之下。
- 主机名: 比如
https://example.com
。 允许加载来自指定主机名的资源。 可以包含端口号,比如https://example.com:8080
。 - 域名通配符: 比如
*.example.com
。 允许加载来自指定域名及其所有子域名的资源。
五、 如何设置CSP:HTTP头部 vs. <meta>
标签
设置CSP有两种方式:
-
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'); });
-
<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-uri
和sandbox
指令。 所以,强烈建议使用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'
是非常不安全的,应该尽量避免使用。 但是,有时候我们确实需要使用内联脚本或样式,比如为了性能优化,或者为了实现一些特殊的效果。 这时候,就可以使用 nonce
或 hash
来更安全地使用内联脚本和样式。
-
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, '<'), // 避免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的最佳实践:
- 使用HTTP头部设置CSP。 避免使用
<meta>
标签。 - 从最严格的策略开始,逐步放宽。 先设置一个非常严格的策略,只允许加载来自自己的服务器的资源,然后逐步添加其他来源。
- 使用“报告模式”测试你的CSP策略。 在不影响用户体验的情况下,收集违规报告,并根据报告来修改你的CSP策略。
- 避免使用
'unsafe-inline'
和'unsafe-eval'
。 如果一定要使用内联脚本或样式,可以使用nonce
或hash
。 - *不要使用通配符 ``。** 这会降低CSP的安全性。
- 定期审查和更新你的CSP策略。 随着你的网站的变化,你的CSP策略也需要更新。
- 使用 Trusted Types API 来防御DOM型的XSS攻击。
- 使用CSP兼容性检查工具。 比如 https://csp-evaluator.withgoogle.com/ 可以帮助你检查你的CSP策略是否存在问题。
十、 总结
好了,今天的CSP小课堂就到这里了。 希望大家通过今天的学习,能够对CSP有一个更深入的了解,并能够在自己的网站上正确地配置CSP,保护用户的安全。
记住: 安全无小事! 只有做好每一个细节,才能让你的网站更加安全可靠。
谢谢大家! 下课!