如何设置你的CSP?

概述

在应用上线前,一般会使用 AppScan 先进行自测,AppScan 用来测试应用程序所有功能的常见的和特定的安全漏洞,如跨站脚本攻击(Cross-Site Scripting,XSS)、SQL 注入(SQL Injection)、跨站请求伪造(Cross-Site Request Forgery,CSRF)和代码注入(Code Injection)等等。本文将重点介绍如何使用 Content Security Policy 来降低 XSS 攻击的风险和影响,而且在配置过程中,我们经常也会遇到各种各样的问题,明明配置了相应的策略,还会出现如下漏洞:

要解决这些问题,我们先来了解 Content Security Policy 是个什么东西。

Content Security Policy

我们知道网络的安全源于同源策略,两个具有相同协议、主机号和端口号的才能进行资源共享,这个策略减少了安全风险,但实际上,攻击者还是可以通过一定的手段绕过同源策略,然后在你的网页上注入恶意代码,比如说一个很典型的例子,留言板 XSS 攻击,攻击者伪装成正常的用户提交一条包含恶意代码的留言存储到数据库,那么其他用户在查看留言板的时候,那些带有恶意代码的留言从数据库加载出来,浏览器并不知道哪些是恶意代码,浏览器发现有 XSS 代码,就当做正常的 html 和 js 解析执行了,于是安全风险就发生了。因为 XSS 攻击手段是利用了浏览器对从服务器收到的内容的信任,所以被攻击人的浏览器就会执行恶意代码,那么 CSP 的作用也就体现出来了。内容安全策略英语:Content Security Policy ,简称CSP)是一种计算机安全标准,旨在防御跨站脚本、点击劫持等代码注入攻击,阻止恶意内容在受信网页环境中执行。网站管理员通过指定可执行脚本的有效来源的域,兼容 CSP 的浏览器将只执行从这些域来的脚本,而忽略所有其他脚本,从而减少 XSS 的传播途径。

快速入门

我们可以先在自己的虚拟机或者服务器里安装一个 nginx,这里省略安装的过程,当我们安装好之后,我们使用 AppScan 去扫描默认页面,会扫描出如下的漏洞:

这个漏洞说明我们的 http://192.168.126.128 返回的 HTTP 标头里缺少了 Content-Security-Policy。

CSP 首选的传输机制是 HTTP 标头,也可以直接在页面中的标签设置策略:

css 复制代码
<meta http-equiv="Content-Security-Policy" content="default-src https://example.com; child-src 'none'; object-src 'none'">

如果要在 HTTP 标头里添加 CSP,我们可以在 nginx 的配置文件里的 location 块中使用 add_header 指令,更改后我们 location 如下:

css 复制代码
location / {
    root   html;
    index  index.html index.htm;
    add_header Content-Security-Policy "default-src 'self'";
}

重启我们的 nginx 之后,刷新浏览器,就可以看到我们的标头里出现 CSP 了:

CSP 的指令

刚刚我们添加上了 CSP 头,但是浏览器的控制台也出现了报错:

这个错误信息表示由于违反了 Content Security Policy(CSP)的规定,浏览器拒绝应用内联样式。在你的 CSP 配置中,指定了 default-src 'self',这意味着只允许加载同源的资源。由于没有明确设置 style-src 指令,浏览器采用了默认的 default-src 指令作为备用。

在解决这个问题之前,我们先来看看 CSP 的指令,CSP 提供了一套丰富的策略指令,可以对页面运行加载的资源进行精确的控制。

  • base-uri 限制了可以出现在页面 元素中的 URL。
  • connect-src 限制了可以连接的来源(通过 XHR、WebSockets 和 EventSource)。
  • font-src 指定了可以提供网络字体的来源,可以通过 font-src cdn.example.com 启用来自 cdn.example.com 的字体。
  • form-action 限制了从 提交的有效端点。
  • frame-ancestors 指定可以嵌入当前页面的源。该指令适用于 、、 和 标记。该指令不能用于 标记,仅适用于非 HTML 资源。
  • img-src 限制了可以加载图像的来源。
  • media-src 限制允许传输视频和音频的来源。
  • object-src 可以控制 Flash 和其他插件。
  • style-src 指定样式表的有效来源。
  • script-src 指定 JavaScript 的有效来源。

CSP的指令有许多,在这里我不一一列出,更多的指令可以浏览以下网址:

developer.mozilla.org/en-US/docs/...

默认情况下,指令是开放的,如果你没有为某个指令(比如 font-src)设置特定的策略,那么默认情况下该指令的行为就好像你指定了 * 作为有效来源,例如,你可以从任何地方加载字体,不受任何限制。

你可以通过指定 default-src 指令来覆盖这一默认行为,该指令为大多数未指定的指令定义默认值。一般来说,这适用于任何以 -src 结尾的指令。如果 default-src 设置为 example.com,而又没有指定 font-src 指令,那么就只能从 example.com 加载字体,而不能从其他地方加载。

以下的指令不使用 default-src 作为备用:

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

这里我们以 style-src 举例,style-src 可以允许一个或多个来源:

css 复制代码
Content-Security-Policy: style-src <source>;
Content-Security-Policy: style-src <source> <source>;

每个指令中的 都很灵活,你可以按 (data:、https:)指定源,也可以按特定范围指定源,从 (example.com,匹配该主机上的任何源:任何方案、任何端口)到完全限定 URI(example.com:443,仅匹配 https、example.com 和 443 端口)。可以使用通配符,但只能作为方案、端口或在主机名最左边的位置使用:://.example.com:*将匹配 example.com 的所有子域(但不包括 example.com 本身),使用任何方案和任何端口。还允许四个关键字 'none'、'self'、'unsafe-inline'、'unsafe-eval'。

互联网主机名或 IP 地址,URL 方案、端口号和路径为可选项。通配符('*')可用于子域、主机地址和端口号,表示每个值的所有合法值都有效。匹配方案时,允许安全升级(例如,指定 example.com 将匹配 example.com)。

  • http://*.example.com:匹配 example.com 的任何子域(例如:mail.example.com),也匹配 https 资源(例如:mail.example.com)。

  • https://*.example.com:12/path/to/file.js:仅当路径为 /path/to/file.js 时,匹配从 example.com 的任何子域使用 https: 并且端口在 12 上的所有加载尝试。

  • data: 允许 data: URLs 用作内容源,但这是不安全的,攻击者也可以注入任意的东西到 data: URLs,请谨慎使用,绝对不要用于脚本。

  • mediastream:允许将 mediastream:URIs 作为内容源。

  • blob:允许将 blob:URIs 用作内容源。

  • filesystem:允许将 filesystem: URIs 用作内容源。

四个关键字

  • 'none' 什么都不匹配。
  • 'self' 匹配当前源点,但不匹配其子域。
  • 'unsafe-inline' 允许内联 JavaScript 和 CSS。
  • 'unsafe-eval' 允许使用 eval() 和其他不安全的方法从字符串创建代码。

这些关键字都需要单引号引起来。例如,script-src 'self' 授权从当前主机执行 JavaScript;script-src self 允许从名为 "self "的服务器(而不是当前主机)执行 JavaScript。

更多细节,你可以浏览以下网址:

developer.mozilla.org/en-US/docs/...

请不要使用内联脚本和样式

上面阐述了很多,我们可以知道 CSP 是通过限制来源列表来指示浏览器哪些来源是允许的哪些来源是不允许的,从而降低 XSS 带来的风险,但是通过限制来源列表并不能解决 XSS 攻击带来的最大威胁------内联脚本注入。如果攻击者能注入一个包含恶意代码的脚本标签(),浏览器是没有办法识别出这是包含恶意代码的内敛标签还是普通的内敛标签,那么 CSP 是通过完全禁止内敛脚本解决这一问题的。它这个禁止的范围不仅包括嵌入在

如此一来,你需要将

xml 复制代码
<script>
    function doSomeThings() {
    	alert('Do Somethings');
    }
</script>
<button onclick='doSomeThings();'>Coding...</button>

改成这样:

xml 复制代码
<!-- index.html -->
<script src='index.js'></script>
<button id='coding'>Coding...</button>
javascript 复制代码
// index.js
function doSomeThings() {
    alert('Do Somethings');
}
document.addEventListener('DOMContentLoaded', function () {
    document.getElementById('coding')
    .addEventListener('click', doSomeThings);
});

内联样式的处理方法与此相同,样式属性和样式标签都应合并到外部样式表中,以防止 CSS 带来 XSS 渗入的途径。如果必须使用内联脚本和样式,可以在 script-src 或 style-src 指令中添加 "unsafe-inline "。也可以使用 nonce 或哈希值,但最好就不要使用,禁止内联脚本是 CSP 提供的最大安全优势,而禁止内联样式也同样会加强应用程序的安全性。如果你有代码的控制能力,能确保将所有代码移出行外后仍能正常工作,那你前期付出一些努力也是值得的。

如果你必须使用到内联脚本和样式的话:

  • 使用 hash

哈希值的工作原理与 nonce 基本相同,假设你的页面包含以下内容:

xml 复制代码
<script>alert('Hello, World.');</script>

你的政策需要这样设置:

css 复制代码
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

这里有几点需要注意,sha*- 前缀指定了生成哈希值的算法。在上面的例子中,使用的是 sha256-。CSP 还支持 sha384- 和 sha512-。生成哈希值时,不要包含

如果你使用的是谷歌浏览器,你打开 DevTools,控制台会有错误信息,在错误信息里就可以看到内联脚本正确的哈希值。使用哈希值通常不是一种很好的方法。如果通过格式化代码等方式更改了脚本标签内的任何内容,甚至是空白格,哈希值就会不同,脚本也就无法使用。

  • 使用 nonce

这种方法稍微复杂一些,但你不必担心格式化代码出现的问题。nonce 是一次性使用的唯一随机值,你可以为每个 HTTP 响应生成该值,并将其添加到 Content-Security-Policy 标头中,例如:

xml 复制代码
<script nonce="fdjkghaidgasmndbfgasasd">
    
</script>

然后将 nonce 添加到 script-src 指令中,并附加到 nonce- 关键字中:

css 复制代码
Content-Security-Policy: script-src 'nonce-fdjkghaidgasmndbfgasasd'

每次页面请求都必须重新生成 nonce,而且它们必须是不可猜测的。

unsafe-eval

'unsafe-eval' 关键字控制着几种从字符串创建代码的脚本执行方法。如果页面有 CSP HTTP 标头,而 script-src 指令钟没有指定 'unsafe-eval',则下列方法将被阻止,不会产生任何效果:

  • eval()
  • Function()
  • setTimeout()
  • setInterval()
  • window.setImmediate
  • window.execScript() Non-standard (IE < 11 only)

如果是你自己的代码,你有代码的控制能力,重构后就可以不使用 eval。如果是依赖程序,比如说使用了某个库,请查阅其文档,看看最新版本或使用它的特定方法是否与安全内容安全策略头兼容,如果没有,你就必须在 script-src 中添加 unsafe-eval 关键字。

解决问题

阐述了这么多原理,让我们回到上面提到的控制台报错的问题:

我们可以看到因为安全策略,nginx 的样式已经加载不出来了,解决这个问题,我们有三种解决办法:

  1. 将样式文件抽取成单独的文件
  2. 使用 hash
  3. 使用 nonce
  4. 使用 unsafe-inline(迫不得已才使用)

在这里我们演示第一和第二种方式:

单独文件

nginx.html 的内容如下:

xml 复制代码
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

我们需要将标签里的内容抽取成 index.css ,并引用:

xml 复制代码
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
css 复制代码
/* index.css */
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }

更新 html 文件并新增 index.css 文件,我们再次刷新浏览器,我们可以看到报错消失了:

hash

在控制台,我们可以看到,谷歌浏览器已经给我们提供了 hash 的值:

我们只需要我们在 nginx 的配置文件中添加如下指令:

css 复制代码
location / {
    root   html;
    index  index.html index.htm;
    add_header Content-Security-Policy "default-src 'self';style-src 'self' 'sha256-4qxDpGEJUcxjIP3NOEWlTKBLTDQ5y6fmRuEEO6ZT9Q0='";
}

更新 nginx 的配置,刷新浏览器,我们可以看到页面正常加载,并且没有报错信息:

发送报告

第一次将 CSP 部署到生产环境中可能会很吓人,这可能导致你的许多页面无法正常运行。你可以先部署一个 Content-Security-Policy-Report-Only 标头,它会将违规行为打印到控制台,但不会强制执行。然后用不同的浏览器进行各种测试,最终再部署。你还可以让浏览器将 JSON 格式的违规报告 POST 到 report-to 指令中指定的位置。

css 复制代码
Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /path;

假如,我们的请求头设置成如下:

css 复制代码
Content-Security-Policy-Report-Only: default-src 'none'; style-src cdn.example.com; report-to /reports

并且 index.html 如下:

xml 复制代码
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>Index</title>
    <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
    Page content
  </body>
</html>

CSP 只允许从 cdn.example.com 加载样式表,但网站却试图从自己的源站点 (example.com) 加载样式表。能够报告 CSP 违规行为的浏览器就会在访问该 index.html 时以 HTTP POST 请求的形式向 example.com/reports 发送以下报告:

json 复制代码
{
  "csp-report": {
    "blocked-uri": "http://example.com/css/style.css",
    "disposition": "report",
    "document-uri": "http://example.com/index.html",
    "effective-directive": "style-src-elem",
    "original-policy": "default-src 'none'; style-src cdn.example.com; report-to /reports",
    "referrer": "",
    "status-code": 200,
    "violated-directive": "style-src-elem"
  }
}

报告在 blocked-uri 中指明了违规资源的完整路径,但情况并非总是如此。例如,当 index.html 试图从 anothercdn.example.com/stylesheet.... 加载 CSS 时,浏览器不会包含完整路径,而只会包含来源(anothercdn.example.com),这样做是为了防止泄露有关跨域资源的敏感信息。

参考

Content security policy

Content-Security-Policy - HTTP | MDN

Content Security Policy (CSP)

相关推荐
叶落阁主13 小时前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
用户962377954483 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机3 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机3 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954483 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star3 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户962377954483 天前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
不可能的是4 天前
前端 SSE 流式请求三种实现方案全解析
前端·http
cipher4 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行7 天前
网络安全总结
安全·web安全