了解 CSP、它的工作原理以及它为何如此出色。您将从头开始构建内容安全策略标头,并学习如何克服过程中的常见问题。让我们开始吧!
内容安全策略是一项出色的浏览器安全功能,可以防止XSS(跨站脚本)攻击。它还废弃了旧的X-Frame-Options标头以防止跨站点框架攻击。
当不受信任的数据在 Web 上下文中被解释为代码时,就会出现 XSS(跨站点脚本)漏洞。它们通常来自:
XSS 攻击通过例如创建恶意链接来利用这些漏洞,当用户打开链接时,这些链接会在目标用户的 Web 浏览器中注入并执行攻击者的 JavaScript 代码。
这是一个易受 XSS 攻击的 PHP 脚本:
echo "Search results for: " . $_GET('search') . "
"
它很容易受到攻击,因为它会不安全地生成 HTML。search
参数编码不正确。攻击者可以创建如下链接,当目标打开它时,该链接将在网站上执行攻击者的 JavaScript 代码:
https://www.example.com/?search=
打开链接会在用户的浏览器中呈现以下 HTML:
Search results for:
有时有一种误解,认为 XSS 漏洞是低严重性错误。他们不是。在其他人的浏览器中执行网站上的 JavaScript 代码的能力相当于登录到托管服务器并为受影响的用户更改 HTML 文件。
因此,XSS 攻击有效地使攻击者以目标用户身份登录,此外还欺骗用户向攻击者提供一些信息(例如他们的密码),或者可能在用户的工作站上下载和执行恶意软件。
而且 XSS 漏洞并不只影响个人用户。存储型 XSS 会影响访问受感染页面的每个人,而反射型 XSS 通常会[像野火一样传播] ( https://en.wikipedia.org/wiki/Samy_(computer_worm))。
CSP 通过以下方式非常有效地防御 XSS 攻击。
通过阻止页面执行内联脚本,像注入这样的攻击
不管用。
通过阻止页面从任意服务器加载脚本,像注入这样的攻击
不管用。
通过阻止页面执行 text-to-JavaScript 函数(也称为DOM-XSS sinks),您的网站将被迫免受以下漏洞的影响。
// A Simple Calculator
var op1 = getUrlParameter('op1')
var op2 = getUrlParameter('op2')
var sum = eval(`${op1} + ${op2}`)
console.log(`The sum is: ${sum}`)
通过限制您网站上的 HTML 表单可以提交其数据的位置,像下面这样注入网络钓鱼表单也不起作用。
通过限制 HTML对象标签,攻击者也无法在页面上注入标签(可以执行 JavaScript 代码)。
通过限制
在页面上插入标签的方式,您将防止攻击者更改页面的基本 URI,这可能导致 JavaScript 从攻击者的服务器加载。
您可以通过两种方式在您的网站上实施内容安全策略。
从您的 Web 服务器发送 Content-Security-Policy HTTP 响应标头。
Content-Security-Policy: ...
首选使用标头并支持完整的 CSP 功能集。在所有 HTTP 响应中发送它,而不仅仅是索引页面。
有时您不能使用 Content-Security-Policy 标头。一个例子是,当您在 CDN 中部署 HTML 文件时,标题不受您的控制。
在这种情况下,您仍然可以通过在 HTML 标记中指定元标记来使用 CSP。
仍然支持几乎所有内容,包括完整的 XSS 防御。但是,您将无法使用框架保护、沙盒或CSP 违规日志记录端点。
是时候构建我们的内容安全策略标头了!我创建了一个小 HTML 文档供我们练习。如果你想跟随,请 fork这个 CodeSandbox,然后打开页面 URL(例如Sandbox - CodeSandbox ,然后在 Google Chrome 浏览器中
这是 HTML:
CSP Practice
CSP Practice
Cat fact:
我们还有app.js
一个微型快递应用程序来设置Content-Security-Policy
标题。现在,它正在发送一个空的 CSP,它什么都不做。
var express = require('express')
var app = express()
const csp = ''
app.use(
express.static('public', {
setHeaders: function (res, path) {
res.set('Content-Security-Policy', csp)
},
})
)
var listener = app.listen(8080, function () {
console.log('Listening on port ' + listener.address().port)
})
如果您查看控制台,则会显示几条消息。
Inline script attack succeeded.
Sourced script attack succeeded.
Good script with jQuery succeeded
在这一点上,CSP 头没有做任何事情,所以一切,无论好坏,都是允许的。您还可以确认在密码网络钓鱼表单中点击“提交”按预期工作(“密码”被发送到 appsecmonkey.com)。
伟大的。让我们开始添加安全性。
default-src是您要添加的第一个指令。如果您没有明确指定它们,它是许多其他指令的后备。
首先设置default-src
为'none'
。单引号是强制性的。如果你只写none
没有单引号,它会引用一个带有 URL 的网站none
,这可能不是你想要的。
let defaultSrc = "default-src 'none'"
const csp = [defaultSrc].join(';')
Content-Security-Policy: default-src 'none'
现在重新启动服务器(左侧有一个机架服务器图标,显示该选项)。正如预期的那样,一切都被打破了。
打开 Chrome 开发者工具,你会发现里面充满了 CSP 违规错误。
注意 您可能会看到 CodeSandbox 客户端挂钩“ https://sse-0.codesandbox.io/client-hook-5.js”的违规行为。忽略这些。
该页面现在已完全损坏,但也很安全。嗯,几乎是安全的。网络钓鱼表单仍然有效,因为该default-src
指令不涵盖该form-action
指令。让我们接下来解决这个问题。
form-action规定了网站可以提交表单的位置。为了防止密码钓鱼表单起作用,让我们像这样更改 CSP。
let defaultSrc = "default-src 'none'"
let formAction = "form-action 'self'"
const csp = [defaultSrc, formAction].join(';')
Content-Security-Policy: default-src 'none';form-action 'self'
刷新页面,并通过尝试提交表单来验证它是否有效。
❌ 拒绝将表单数据发送到“ https://www.appsecmonkey.com/evil”,因为它违反了以下内容安全策略指令:“form-action 'self'”。
美丽的。按预期工作。
在稍微放宽政策以使我们的页面正确加载之前,让我们再添加一个限制。也就是说,让我们通过将frame-ancestors设置为来防止其他页面陷害我们'none'
。
let frameAncestors = "frame-ancestors 'none'"
const csp = [defaultSrc, formAction, frameAncestors].join(';')
Content-Security-Policy: default-src 'none';form-action 'self';frame-ancestors 'none'
如果您检查 CodeSandbox 浏览器,您将看到它无法再在框架中显示您的页面。从这里开始,将您的沙盒网站的 URL 复制到另一个可以查看的选项卡中。
好吧。否认就够了。接下来让我们允许一些事情。
查看控制台,第一个违规行为是:
❌ 拒绝加载样式表“ https://lqil3.sse.codesandbox.io/stylesheets/style.css”,因为它违反了以下内容安全策略指令:“default-src 'none'”。请注意,'style-src-elem' 没有明确设置,所以 'default-src' 用作后备。
❌ 拒绝加载样式表' https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap',因为它违反了以下内容安全策略指令:“style-src 'self'”。请注意,'style-src-elem' 没有显式设置,因此 'style-src' 用作后备。
您可以使用style-src指令解决此问题,允许样式表从同一来源和 google 字体加载。
...
let styleSrc = "style-src";
styleSrc += " 'self'";
styleSrc += " https://fonts.googleapis.com/";
const csp = [defaultSrc, formAction, frameAncestors, styleSrc].join(";");
Content-Security-Policy: default-src 'none';form-action 'self';frame-ancestors 'none';style-src'self' https://fonts.googleapis.com/
刷新页面,哇!这样的风格。
让我们继续讨论图像。
而不是美丽的红点,我们有以下错误:
❌ 拒绝加载图像 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA%0A AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO%0A 9TXL0Y4OHwAAAABJRU5ErkJggg==',因为它违反了以下内容安全策略指令:“default-src.'none 请注意,'img-src' 没有显式设置,所以 'default-src' 用作后备
我们可以像这样使用img-src指令修复我们的图像。
let imgSrc = 'img-src'
imgSrc += " 'self'"
imgSrc += ' data:'
const csp = [defaultSrc, formAction, frameAncestors, styleSrc, imgSrc].join(';')
Content-Security-Policy: default-src 'none';form-action 'self';frame-ancestors 'none';style-src'self' https://fonts.googleapis.com/;img-src 'self' data:
我们允许来自我们自己来源的图像。我们还允许使用数据 URL,因为它们在优化的网站中很常见。
刷新页面,然后……是的!尽显荣耀的红点。
如果您允许来自任何 URL 的图像,那么攻击者就有可能通过悬空标记攻击绕过您的 CSP 。
至于我们的字体,我们有以下错误。
❌ 拒绝加载字体“ https://fonts.gstatic.com/s/roboto/v20/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2”,因为它违反了以下内容安全策略指令:“default-src 'none'”。请注意,'font-src' 没有明确设置,所以 'default-src' 用作后备
我们可以通过添加font-src指令使其消失,如下所示:
let fontSrc = 'font-src'
fontSrc += ' https://fonts.gstatic.com/'
const csp = [defaultSrc, formAction, frameAncestors, styleSrc, imgSrc, fontSrc].join(';')
Content-Security-Policy: default-src 'none';form-action 'self';frame-ancestors 'none';style-src'self' https://fonts.googleapis.com/;img-src 'self' data:;font-src https://fonts.gstatic.com/
好吧,现在它变得真实了。script-src可以说是 CSP 存在的主要原因,在这里我们可以制定或破坏我们的策略。
让我们看看例外情况。第一个是“攻击者”的内联脚本。我们不想通过任何指令来允许它,所以让我们继续阻止它。
❌ 拒绝执行内联脚本,因为它违反了以下内容安全策略指令:“default-src 'none'”。启用内联执行需要“unsafe-inline”关键字、哈希(“sha256-OScJmDvbn8ErOA7JGuzx/mKoACH2MwrD/+4rxLDlA+k=”)或随机数(“nonce-...”)。请注意,'script-src' 没有显式设置,因此 'default-src' 用作后备。
第二个是攻击者的源脚本。让我们继续阻止这个。
❌ 拒绝加载脚本“ https://www.appsecmonkey.com/evil.js”,因为它违反了以下内容安全策略指令:“default-src 'none'”。请注意,'script-src-elem' 没有显式设置,因此 'default-src' 用作后备。
然后是我们想要允许的谷歌分析。
❌ 拒绝加载脚本“ https://www.google-analytics.com/analytics.js”,因为它违反了以下内容安全策略指令:“default-src 'none'”。请注意,'script-src-elem' 没有显式设置,因此 'default-src' 用作后备。
我们还希望允许使用 jQuery。
❌ 拒绝加载脚本“ https://code.jquery.com/jquery-1.12.4.js”,因为它违反了以下内容安全策略指令:“default-src 'none'”。请注意,'script-src-elem' 没有显式设置,因此 'default-src' 用作后备。
最后,我们要允许获取猫事实的脚本。
❌ 拒绝执行内联脚本,因为它违反了以下内容安全策略指令:“default-src 'none'”。启用内联执行需要“unsafe-inline”关键字、哈希(“sha256-dsERlyo3ZLeOnlDtUAmCoZLaffRg2Fi9LTWvmIgrUmE=”)或随机数(“nonce-...”)。另请注意,'script-src' 未明确设置,因此 'default-src' 用作后备。
让我们从简单的开始。通过将 Google 分析和 jQuery URL 添加到我们的政策中,我们可以摆脱这两种违规行为。此外,添加“self”以准备下一步(将 cat 事实脚本重构为单独的 JavaScript 文件)。
let scriptSrc = 'script-src'
scriptSrc += " 'self'"
scriptSrc += ' https://www.google-analytics.com/analytics.js'
scriptSrc += ' https://code.jquery.com/jquery-1.12.4.js'
const csp = [defaultSrc, formAction, frameAncestors, styleSrc, imgSrc, fontSrc, scriptSrc].join(';')
处理内联脚本的首选方法是将它们重构为自己的 JavaScript 文件。所以删除 cat 事实脚本标签并将其替换为以下内容:
...
Cat fact:
...
并将脚本的内容移动到javascripts/cat-facts.js
如下所示:
$(document).ready(function () {
$.ajax({
url: 'https://cat-fact.herokuapp.com/facts/random',
type: 'GET',
crossDomain: true,
success: function (response) {
var catFact = response.text
$('#cat-fact').text(catFact)
},
error: function (xhr, status) {
alert('error')
},
})
console.log(`Good script with jQuery succeeded`)
})
现在刷新,然后......真可惜。在我们获胜之前再处理一次违规行为!
❌ 拒绝连接到“ https://cat-fact.herokuapp.com/facts/random”,因为它违反了以下内容安全策略指令......
connect-src指令限制网站可以连接的位置。目前,它阻止我们获取猫的事实。让我们修复它。
let connectSrc = 'connect-src'
connectSrc += ' https://cat-fact.herokuapp.com/facts/random'
const csp = [
defaultSrc,
formAction,
frameAncestors,
styleSrc,
imgSrc,
fontSrc,
scriptSrc,
connectSrc,
].join(';')
刷新页面。呸!该页面有效,而攻击则无效。你可以在这里尝试完成的网站。这是我们想出的:
Content-Security-Policy: default-src 'none'; form-action 'self'; frame-ancestors 'none'; style-src 'self' https://fonts.googleapis.com/; img-src 'self' data:; font-src https://fonts.gstatic.com/; script-src 'self' https://www.google-analytics.com/analytics.js https://code.jquery.com/jquery-1.12.4.js; connect-src https://cat-fact.herokuapp.com/facts/random
让我们将它插入Google 的 CSP 评估器,看看我们是怎么做的。
非常好。中的黄色script-src
是因为我们使用了“self”,例如,如果您托管用户提交的内容,这可能会出现问题。
但这是一个阳光灿烂的日子,我们能够重构代码并摆脱内联脚本和危险的函数调用。现在让我们看看当您被迫使用一个使用 eval 的 JavaScript 框架或当您需要在 HTML 中包含内联脚本时,您可以做些什么。
假设您无法摆脱内联 JavaScript。从内容安全策略版本 2 开始,您可以使用script-src 'sha256-
允许执行具有特定哈希的脚本。浏览器很好地支持随机数和散列;有关详细信息的兼容性,请参见此处。无论如何,只要您正确使用 CSP,它就是向后兼容的。
您可以通过分叉此 CodeSandbox来跟进。和之前的情况一样,但是这次我们不会将内联脚本重构为自己的文件。相反,我们会将其哈希添加到我们的策略中。
您可以手动获取 SHA256 哈希,但要正确设置空格和格式是很棘手的。幸运的是,您可能已经注意到,Chrome 开发者工具为我们提供了哈希值。
❌ 拒绝执行内联脚本,因为它违反了以下内容安全策略指令:“script-src 'self' https://www.google-analytics.com/analytics.js https://code.jquery.com/jquery- 1.12.4.js”。启用内联执行需要“unsafe-inline”关键字、哈希(“sha256-V2kaaafImTjn8RQTWZmF4IfGfQ7Qsqsw9GWaFjzFNPg=”)或随机数(“nonce-...”)。
因此,让我们将该哈希添加到我们的策略中,页面将再次运行。
...
scriptSrc += " 'sha256-V2kaaafImTjn8RQTWZmF4IfGfQ7Qsqsw9GWaFjzFNPg='";
scriptSrc += " 'unsafe-inline'";
...
我们还必须添加unsafe-inline
向后兼容性。不用担心; 对于支持 CSP 级别 2 的浏览器,如果存在散列或随机数,浏览器会忽略它。
注意 使用散列通常不是一个很好的方法。如果您更改脚本标签内的任何内容(甚至是空格),例如通过格式化您的代码,散列值将会不同,并且脚本不会呈现。这一步应该在您的构建管道中自动化,以使其可靠地工作。
允许特定内联脚本的第二种方法是使用nonce。它稍微复杂一些,但您不必担心格式化代码。
随机数是您为每个 HTTP 响应生成的唯一一次性随机值,并添加到 Content-Security-Policy 标头中,如下所示:
const nonce = uuid.v4()
scriptSrc += ` 'nonce-${nonce}'`
然后,您可以将此 nonce 传递给您的视图(使用 nonce 需要非静态 HTML)并呈现如下所示的脚本标签:
并且test.js
是一个加载另外三个脚本的脚本:
function dynamicallyLoadScript(url) {
var script = document.createElement('script')
script.src = url
document.head.appendChild(script)
}
dynamicallyLoadScript('https://code.jquery.com/jquery-3.6.0.min.js')
dynamicallyLoadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css')
dynamicallyLoadScript('https://d3js.org/d3.v6.min.js')
如果我们在 Firefox 中打开此页面,我们会收到以下错误:
❌ 内容安全策略:该页面的设置阻止了在https://code.jquery.com/jquery-3.6.0.min.js(“script-src ”)加载资源。test.js:5:16
❌ 内容安全策略:该页面的设置阻止了在https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css(“script-src ”)加载资源。test.js:5:16
❌ 内容安全策略:该页面的设置阻止了在https://d3js.org/d3.v6.min.js(“script-src ”)加载资源。test.js:5:16
现在这里是另一个 CodeSandbox。这次我们在我们的script-src
like中添加了一个东西:
let scriptSrc = 'script-src'
scriptSrc += ` 'nonce-${nonce}'`
scriptSrc += " 'strict-dynamic'" // Propagate trust to scripts loaded by already trusted scripts
现在,如果我们加载页面,我们可以看到错误消失了,因为这'strict-dynamic'
意味着test.js
已经被 nonce 信任的脚本被允许将该信任传播到它加载的任何脚本。
但是,如果我们在还不支持严格动态的浏览器(例如 Safari 15.3 或更早版本)中打开此页面怎么办?
❌ [错误]拒绝加载https://code.jquery.com/jquery-3.6.0.min.js,因为它没有出现在内容安全策略的 script-src 指令中。
❌ [错误]拒绝加载https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css因为它没有出现在内容安全策略的 script-src 指令中.
❌ [错误]拒绝加载https://d3js.org/d3.v6.min.js,因为它没有出现在内容安全策略的 script-src 指令中。
脚本不会加载,因为 Safari 15.3 还不支持strict-dynamic
。所以我们需要做的是为 Safari 用户添加一个后备,以允许从任何地方通过 HTTPS 加载脚本。
let scriptSrc = 'script-src'
scriptSrc += ` 'nonce-${nonce}'`
scriptSrc += ` https:`
scriptSrc += " 'strict-dynamic'"
现在最新的 Safari 支持严格动态,我衷心推荐你使用它,https:
为过时的浏览器添加后备。
这个 CodeSandbox在所有最新的主流浏览器上都是安全的,它也不会在过时的浏览器上破坏网站。
该'unsafe-hashes'
指令允许 DOM 事件处理程序执行在script-src
.
例如,我们可以alert("hello")
通过 `script-src 'sha256-15xTQOuF/OesomfBHh+sYeg4tGStBBWrw6CRoP9zLjk=' 来允许。
let scriptSrc = 'script-src'
scriptSrc += ` 'sha256-15xTQOuF/OesomfBHh+sYeg4tGStBBWrw6CRoP9zLjk='`
现在我们可以把它放在网站上,它会起作用:
但是,这不适用于大多数浏览器。
让我们在 Chrome 中尝试一下:
❌ 拒绝执行内联事件处理程序,因为它违反了以下内容安全策略指令:“script-src 'sha256-15xTQOuF/OesomfBHh+sYeg4tGStBBWrw6CRoP9zLjk='”。
现在让我们添加'unsafe-hashes'
选项。
let scriptSrc = 'script-src'
scriptSrc += ` 'sha256-15xTQOuF/OesomfBHh+sYeg4tGStBBWrw6CRoP9zLjk='`
scriptSrc += ` 'unsafe-hashes'`
现在脚本在单击按钮时执行。你可以在这里尝试或 fork CodeSandbox 在这里玩
不安全哈希安全注意事项
该指令带有前缀的原因unsafe-
是它允许 XSS 攻击调用任何已使用哈希列入白名单的函数。
假设有一个如下所示的 UI:
Account Settings
并且开发人员已将deleteAccount()
哈希列入白名单script-src
:
scriptSrc += " 'sha256-nh5C95kYk07xMaWT0ZEbfCqzCKDC1cpLP0hF+hqkYN4='"
现在攻击者可以做的是注入一个 XSS 有效载荷,如下所示:
并且用户的帐户将被删除。
出于这个原因,最好删除此类内联事件处理程序并将处理程序附加到受信任的 JavaScript 代码中,如下所示:
document.getElementById('btnDeleteAccount').addEventListener('click', function () {
deleteAccount()
})
不安全哈希浏览器支持
正如您在此处看到的,浏览器支持unsafe-hashes
仍在等待 Firefox,因此我们还不能真正依赖此功能。也没有明智的后备选项。
从 CSP 级别 3 开始,该script-src
指令已分为两部分:script-src-elem
和script-src-attr
. 这为您提供了更精细的控制,因为现在您可以使用script-src-elem
来限制script
标签和script-src-attr
限制内联事件处理程序。
Firefox 或 Safari也不支持这些,尽管 Safari 已经在预览中。
现在,这是一个有趣的功能。受信任的类型使您可以在以特定方式创建输入时允许注入接收器。注入水槽到底是什么鬼?忍受我。我们将很快介绍一个示例。
注入水槽
注入接收器是指在使用不受信任的数据调用时将直接导致 JavaScript 代码执行的函数和属性。其中包括允许攻击者直接修改 HTML 的 HTML 注入接收器,以及允许攻击者执行任意 JavaScript 代码的DOM XSS 注入接收器。element.innerHTMLeval()
可信类型
可信类型是一种 API,它允许应用程序锁定强大的 API 以仅接受不可欺骗的类型值代替字符串,以防止将这些 API 与攻击者控制的输入一起使用而导致漏洞。require-trusted-types-for
它以andtrusted-types
指令的形式与 CSP 集成。
创建策略
该require-trusted-types-for
指令告诉浏览器不允许 JavaScript 代码使用任何归类为注入接收器的函数或属性,除非用于调用函数或属性的输入类型是受信任的类型。
这是具有以下策略的示例:
let scriptSrc = 'script-src '
scriptSrc += " 'self'"
scriptSrc += " 'unsafe-inline'"
let requireTrustedTypesFor = 'require-trusted-types-for'
requireTrustedTypesFor += " 'script'"
目前,您可以提供的唯一值require-trusted-types-for
是script
.
然后我们让这个 HTML 文件加载一个脚本:
内容test.js
如下:
let payload = "CLICK ME
"
document.getElementById('htmlOutput').innerHTML = payload
现在如果我们在 Firefox 中加载还不支持可信类型的页面,“攻击”将会成功(因为我们指定了unsafe-inline
)。您可以通过在 Firefox 中打开此页面并单击“CLICK ME”文本来验证这一点。
但是,如果我们将它加载到支持受信任类型的 Google Chrome 中,则会启动并require-trusted-types-for
阻止.innerHTML()
调用。
❌ test.js:2 未捕获的类型错误:无法在“元素”上设置“innerHTML”属性:此文档需要“TrustedHTML”分配。
现在是施展魔法的时候了。我们将定义一个名为的策略my-policy
,我们可以使用它来创建受信任的类型。首先让我们修复我们的 CSP 标头以允许该策略。这就是trusted-types
指令的用武之地。
let requireTrustedTypesFor = 'require-trusted-types-for'
requireTrustedTypesFor += " 'script'"
let trustedTypes = 'trusted-types'
trustedTypes += ' my-policy'
我们的想法是,我们只允许调用一个策略my-policy
,并且在攻击者有机会运行任何漏洞利用之前,我们在 JavaScript 代码中定义它(策略一旦创建就无法更改)。
现在让我们打开test.js
,定义我们的策略并开始使用它!
// Define a sanitizer function
const mySanitize = (dirty) => dirty.replace(/CLICK ME"
if (myPolicy) {
// If trusted types supported, we call the policy's createHTML function to return a trusted type
payload = myPolicy.createHTML(payload)
} else {
// If not then we sanitize anyway calling the sanitizer directly
payload = mySanitize(payload)
}
document.getElementById('htmlOutput').innerHTML = payload
这个简单的策略对任何字符进行 HTML 编码<
,使 XSS 攻击更加困难。这不是一个很好的策略,但对于我们的示例来说已经足够了。
现在该页面将在 Firefox 和 Chrome 上加载,通过 Chrome 将强制执行受信任的类型,而 Firefox 则不会。您可以在这里尝试或在此处分叉 CodeSandbox
更有用的政策
我们可以使用DOMPurify库已经创建的策略,而不是创建我们自己的策略。
让我们首先创建一个 CSP,它允许加载 DOMPurify 脚本,需要脚本的可信类型并允许 DOMPurify 定义的策略。
let scriptSrc = 'script-src '
scriptSrc += " 'self'"
scriptSrc += " 'unsafe-inline'"
scriptSrc += ' https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js'
let requireTrustedTypesFor = 'require-trusted-types-for'
requireTrustedTypesFor += " 'script'"
let trustedTypes = 'trusted-types'
trustedTypes += ' dompurify'
然后我们将在您的页面中加载 DOMPurify:
最后,我们将在使用之前使用 DOMPurify 来清理我们的有效负载。
let payload = "CLICK ME
"
document.getElementById('htmlOutput').innerHTML = DOMPurify.sanitize(payload, {
RETURN_TRUSTED_TYPE: true,
})
而已!-tag 现在将h1
在 Firefox 和 Chrome 上正确显示,在 Chrome 上强制执行受信任的类型,并清理 HTML,以便如果您使用开发人员工具检查标签,您将看到onClick
处理程序已被 DOMPurify 删除。
CLICK ME
您可以在这里尝试并在此处分叉 CodeSandbox 。
可信类型浏览器支持
在撰写本文时,Firefox 或 Safari 尚不支持可信类型。请参阅此处了解最新状态。
内容安全策略头是针对 XSS 攻击的出色防御。需要做一些工作才能做到正确,但这是值得的。
始终首选重构代码以使用安全和干净的策略运行。但是当 inline-scripts 或 eval 无能为力时,CSP 级别 2 为我们提供了我们可以使用的随机数和哈希值。
CSP 3 级有一些非常简洁的功能,但并非所有这些功能都得到 Chrome 和 Edge 以外的任何其他浏览器的很好支持,因此请谨慎使用。幸运的是,我们已经可以使用严格动态。这是一个基于严格动态的通用的、相当严格的 CSP,它应该适用于大多数应用程序。请注意,这unsafe-inline
只是作为古代浏览器的后备方案,只要您有随机数或哈希值,就无需担心。
Content-Security-Policy:
default-src 'self';
frame-ancestors 'self';
form-action 'self';
object-src 'none';
script-src 'nonce-{random}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
base-uri 'none';
report-uri https://cspreport.example.com/
在将执行策略部署到生产环境之前,请从仅报告标题开始,以避免不必要的麻烦。