内容安全策略 (CSP)

了解 CSP、它的工作原理以及它为何如此出色。您将从头开始构建内容安全策略标头,并学习如何克服过程中的常见问题。让我们开始吧!

什么是内容安全策略 (CSP)?

内容安全策略是一项出色的浏览器安全功能,可以防止XSS(跨站脚本)攻击。它还废弃了旧的X-Frame-Options标头以防止跨站点框架攻击。

什么是 XSS 漏洞?

当不受信任的数据在 Web 上下文中被解释为代码时,就会出现 XSS(跨站点脚本)漏洞。它们通常来自:

  1. 不安全地生成 HTML(在没有正确编码的情况下进行参数化)。
  2. 允许用户直接编辑 HTML(例如 WYSIWYG 编辑器)。
  3. 允许用户上传 HTML/SVG 文件并以不安全的方式返回这些文件。
  4. 允许用户设置链接的 HREF 属性。
  5. 不安全地使用 JavaScript(将不受信任的数据传递到可执行函数/属性中)。
  6. 使用过时且易受攻击的 JavaScript 包。

XSS 攻击通过例如创建恶意链接来利用这些漏洞,当用户打开链接时,这些链接会在目标用户的 Web 浏览器中注入并执行攻击者的 JavaScript 代码。

一个简单的例子

这是一个易受 XSS 攻击的 PHP 脚本:

echo "

Search results for: " . $_GET('search') . "

"

它很容易受到攻击,因为它会不安全地生成 HTML。search参数编码不正确。攻击者可以创建如下链接,当目标打开它时,该链接将在网站上执行攻击者的 JavaScript 代码:

https://www.example.com/?search=

打开链接会在用户的浏览器中呈现以下 HTML:

Search results for:

为什么 XSS 漏洞如此危险?

有时有一种误解,认为 XSS 漏洞是低严重性错误。他们不是。在其他人的浏览器中执行网站上的 JavaScript 代码的能力相当于登录到托管服务器并为受影响的用户更改 HTML 文件。

因此,XSS 攻击有效地使攻击者以目标用户身份登录,此外还欺骗用户向攻击者提供一些信息(例如他们的密码),或者可能在用户的工作站上下载和执行恶意软件。

而且 XSS 漏洞并不只影响个人用户。存储型 XSS 会影响访问受感染页面的每个人,而反射型 XSS 通常会[像野火一样传播] ( https://en.wikipedia.org/wiki/Samy_(computer_worm))。

CSP 如何防御 XSS 攻击?

CSP 通过以下方式非常有效地防御 XSS 攻击。

1. 限制内联脚本

通过阻止页面执行内联脚本,像注入这样的攻击

不管用。

2. 限制远程脚本

通过阻止页面从任意服务器加载脚本,像注入这样的攻击

不管用。

3.限制不安全的Javascript

通过阻止页面执行 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}`)

4. 限制表单提交

通过限制您网站上的 HTML 表单可以提交其数据的位置,像下面这样注入网络钓鱼表单也不起作用。

Session expired! Please login again.

5.限制对象

通过限制 HTML对象标签,攻击者也无法在页面上注入标签(可以执行 JavaScript 代码)。

6.限制基础URI

通过限制在页面上插入标签的方式,您将防止攻击者更改页面的基本 URI,这可能导致 JavaScript 从攻击者的服务器加载。

我该如何使用它?

您可以通过两种方式在您的网站上实施内容安全策略。

1. Content-Security-Policy 标头

从您的 Web 服务器发送 Content-Security-Policy HTTP 响应标头。

Content-Security-Policy: ...

首选使用标头并支持完整的 CSP 功能集。在所有 HTTP 响应中发送它,而不仅仅是索引页面。

2. Content-Security-Policy 元标记

有时您不能使用 Content-Security-Policy 标头。一个例子是,当您在 CDN 中部署 HTML 文件时,标题不受您的控制。

在这种情况下,您仍然可以通过在 HTML 标记中指定元标记来使用 CSP。


仍然支持几乎所有内容,包括完整的 XSS 防御。但是,您将无法使用框架保护、沙盒或CSP 违规日志记录端点。

制定您的政策

是时候构建我们​​的内容安全策略标头了!我创建了一个小 HTML 文档供我们练习。如果你想跟随,请 fork这个 CodeSandbox,然后打开页面 URL(例如Sandbox - CodeSandbox ,然后在 Google Chrome 浏览器中

这是 HTML:


  
    CSP Practice
    
    
    
  

  
    

CSP Practice

Cat fact:

Failed to show image.

我们还有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)
})

内容安全策略 (CSP)_第1张图片

 

如果您查看控制台,则会显示几条消息。

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'

现在重新启动服务器(左侧有一个机架服务器图标,显示该选项)。正如预期的那样,一切都被打破了。

内容安全策略 (CSP)_第2张图片

 打开 Chrome 开发者工具,你会发现里面充满了 CSP 违规错误。

内容安全策略 (CSP)_第3张图片

 

注意 您可能会看到 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 复制到另一个可以查看的选项卡中。

内容安全策略 (CSP)_第4张图片

 

好吧。否认就够了。接下来让我们允许一些事情。

样式-src

查看控制台,第一个违规行为是:

❌ 拒绝加载样式表“ 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/

刷新页面,哇!这样的风格。

内容安全策略 (CSP)_第5张图片

让我们继续讨论图像。

img-src

而不是美丽的红点,我们有以下错误:

❌ 拒绝加载图像 '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,因为它们在优化的网站中很常见。

刷新页面,然后……是的!尽显荣耀的红点。

 内容安全策略 (CSP)_第6张图片

 

如果您允许来自任何 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如下所示:

内容安全策略 (CSP)_第7张图片

$(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 评估器,看看我们是怎么做的。

 内容安全策略 (CSP)_第8张图片

 

非常好。中的黄色script-src是因为我们使用了“self”,例如,如果您托管用户提交的内容,这可能会出现问题。

但这是一个阳光灿烂的日子,我们能够重构代码并摆脱内联脚本和危险的函数调用。现在让我们看看当您被迫使用一个使用 eval 的 JavaScript 框架或当您需要在 HTML 中包含内联脚本时,您可以做些什么。

脚本-src:哈希

假设您无法摆脱内联 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

允许特定内联脚本的第二种方法是使用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-srclike中添加了一个东西:

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,因此我们还不能真正依赖此功能。也没有明智的后备选项。

脚本-src-elem & 脚本-src-attr

从 CSP 级别 3 开始,该script-src指令已分为两部分:script-src-elemscript-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-forscript.

然后我们让这个 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(/

这个简单的策略对任何字符进行 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/

在将执行策略部署到生产环境之前,请从仅报告标题开始,以避免不必要的麻烦。

你可能感兴趣的:(前端,javascript,servlet)