前端必备的 web 安全知识手记

2,704 阅读11分钟

前言

安全这种东西就是不发生则已,一发生则惊人。作为前端,平时对这方面的知识没啥研究,最近了解了下,特此沉淀。文章内容包括以下几个典型的 web 安全知识点:XSS、CSRF、点击劫持、SQL 注入和上传问题等(下文以小王代指攻击者),话不多说,我们直接开车🚗(附带的例子浅显易懂哦😯)。

XSS(Cross-site scripting)

XSS 中文叫做跨站脚本攻击。
一句话解释:小王在网页中注入恶意代码,当用户访问页面时,代码会自动运行。
具体点说:凡是在页面中可输入的地方(如地址栏、搜索框和评论区等等)都有隐藏的风险,小王可以在这些地方输入一些特殊的代码(形如 <script>alert(1)</script>),于是乎本来只是单纯的文本变成了可执行的代码,从而造成了攻击。你可能会觉得 alert(1) 能有多大点事,但把它换成别的威力就不一样了,它可以重定向到一个由小王控制的页面、冒充用户发送请求、窃取用户信息发送到小王的服务器上等等。当然了,这还不够具体,例子才是最直白的👇。

举个栗子

因为 XSS 攻击有反射型、存储型和 DOM 型三种类型,所以我们每种都举个简单的栗子🌰:

反射型

反射型 XSS 主要有以下两个步骤:

  1. 假设我们访问一个 www.a.com/?name=hh 页面,后端会把其中的 name 参数取出来并拼接到 html 中(形如 <p>你好啊,hh</p>)返回给用户。
  2. 这时如果我们访问 www.a.com/?name=hh<script>alert(1)</script> 页面,此时后端会返回形如 <p>你好啊,hh<script>alert(1)</script></p> 这样的一个 html,其中的 script 脚本就会在我们的浏览器上加载执行,从而造成攻击。

你可能会觉得这种 url 看起来就有嫌疑,我们一般不访问,但如果把这个 url 转成一个短网址(所谓短网址就是把 url 转成一个 看起来简约又正常的链接),并在网上散播(通常伴随一些美女图片、外挂和金钱💰等一些具有诱惑力的内容),从而骗取用户点击这个 url,造成攻击。所以日常生活中不要乱扫二维码、乱点链接。

当然了,拼接的形式多种多样:

拼接前拼接字符串拼接后
<div>#{msg}</div><script>alert(1)</script><div><script>alert(1)</script></div>
<img src="#{image}" />1" onerror="alert(1)<img src="1" onerror="alert(1)" />
var data = "#{data}"hello";alert(1);"var data = "hello";alert(1);""

存储型

这种类型一般出现在评论区、论坛、留言等类似的地方,基本流程是小王提交了一个恶意评论(就是包含一些恶意代码,和上面类似,形如这样的评论 <img src="1" onerror="alert(1)" />),只不过该评论被保存到了数据库中,而评论对大家又都是可见的,所以任何用户访问该页面时,恶意代码就会从数据库中取出,拼接在 html 中返回给用户,页面加载评论的同时也执行了其中的恶意代码。与反射型相比,存储型的辐射范围更加广泛,处理起来也较为麻烦,有时还需要查好多库删好几张表。

DOM 型

典型的例子就是你连接了一个公共 wifi,浏览页面的时候在底部或者四周有时会有一些小广告的出现,因为这种网络劫持会动态修改页面的内容,比如在 html 中追加一些广告或暗链(暗链就是偷偷加入几个 a 标签,一般可用来增加搜索排名),要注意的是这种攻击是不涉及到服务器的,它的核心是动态修改 dom。

小小总结

总的来说就是页面可输入或拼接显示的地方就可能会有潜在的风险。我们演示个小的 demo,虽然实际上不是这样的😂,但意思到了: 当我们第一次输入 <script>alert(1)</script> 的时候,alert 不生效是因为 H5 中规定用 innerHTML 插入的 script 将不被执行,但是我们可以写别的,比如 <img src='x' onerror="alert(1)">,此时 alert 就被执行了,这个在 MDN 上有明确的说明,这里给大家截图看一下👀: 作为前端平时要注意 innerHTML 等一些有字符串拼接并展示的地方。

防御措施

转义和过滤:

一般来可以使用编码或转义的方式来防御 XSS 攻击,就像下面这样:

function escape(str) {
    if (!str) return ''
    str = str.replace(/&/g, '&amp;')
    str = str.replace(/</g, '&lt;')
    str = str.replace(/>/g, '&gt;')
    str = str.replace(/"/g, '&quot;')
    str = str.replace(/'/g, '&#39;')
    return str
}

对于富文本内容,因为其需要保留 html,所以不好直接使用转义的方法,通常使用白名单过滤(就是允许特定的 html 标签和属性),以抵御 XSS 攻击。当然了更不要相信用户的任何输入,对用户的输入进行特殊字符(如尖括号)的检查也是有必要的。

CSP(Content Security Policy)

这个其实就是设置个响应头信息(Content-Security-Policy),告诉浏览器哪些资源是可加载执行的(实质就是白名单制度),为对应的内容指定相应的策略。具体设置大家可以查阅 MDN,这里截个 github 的图作为示例: 或者加个 meta 标签:

<meta http-equiv="Content-Security-Policy" content="img-src 'self';">

其他

  • HttpOnly:禁止 js 读取 cookie
  • 限制输入内容长度:提高门槛
  • 敏感操作使用验证码

CSRF(Cross-site request forgery)

CSRF 中文叫做跨站请求伪造。 一句话解释:小王盗用你的身份,以你的名义发送恶意请求(发邮件、转账之类的)。

举个栗子

这个没有分类,所以我们直接上栗子🌰:

  1. 用户登录了 www.a.com,登录态被保存在 cookie 中(这是前提)。
  2. 假设 www.a.com 页面有个转账的功能,形如:https://www.a.com/index.php?money=100&to=xx,这个请求能够把 100 块转到 xx 同学的账户上。
  3. 小王引诱用户打开了 www.b.com 页面,而这个页面有一个形如 <img src="https://www.a.com/index.php?money=100&to=wo"> 这样的标签,于是浏览器就会自动发起一个转账请求,并且这个请求会携带上 a 网站的 cookie(即便这个标签根本不在 www.a.com 内),所以能请求成功。
  4. 用户并不晓得,这很可怕😱。所以有些网站会要求已登录的用户在一些关键步骤时再次输入密码就有一定道理了。

防御措施

CSRF 攻击通常有以下两个特点:

  • 通常发生在第三方网站。
  • 小王并不能获取到 cookie,他只是借用请求,有点借刀杀人的意思。

针对以上两点我们可以有以下几种预防措施:

referer

因为请求头中的 referer 记录了请求的来源地址,referer 会指向 b 网站,所以我们可以对 referer 进行检测,以此来禁止来自第三方网站的请求。 但每个浏览器对于 referer 的具体实现可能有差别,也不能保证浏览器自身没有安全漏洞。在有些情况下,攻击者甚至可以隐藏、修改自己请求的 referer。另外 origin 也是一样的道理(origin 只包含了域名信息,不包括具体的 url 路径)

token

由于 CSRF 是利用了浏览器自动携带 cookie 的特性,所以我们可以多加一个校验的字段(不通过 cookie 传递),简单点说就是:

  1. 登录的时候服务端会返回一个 token 值给前端。
  2. 前端在发送请求的时候再把这个 token 当作请求数据传递或者放在请求头里。
  3. 后端会对这个 token 值进行校验。而由 CSRF 的请求是没有携带 token 的,所以不会成功。

至于 token 是啥,这里简单说明以下,其实它是个字符串,你可以当做个临时 id,一般由 userId、随机数、时间戳通过散列算法得到(形如:let token = md5('userId' + 'time' + 'Math.random() + '...'),其实就是调用一个函数)。

加验证码

加验证码的本质和 token 是一样的,这两种类型的原理都是在 cookie 之外的某个地方多增加一个字段供后端校验,这里增加的字段就是验证码。这样一来第三方只能携带上 cookie 但是带不上这个验证码字段,也就预防了 csrf 攻击。

SameSite

可以通过设置 SameSite 的值来禁止第三方网站携带 cookie。SameSite 有三个值可以设置:Strict、Lax、None。具体作用如下:

  • Strict:完全禁止第三方 cookie,跨站点时,任何情况下都不会发送 cookie。
  • Lax:大多数情况也是不发送第三方 cookie,但是导航到目标网址的 get 请求除外。Chrome 计划将 Lax 变为默认设置。
  • None:可以显式关闭 SameSite 属性,不过必须同时设置 Secure 属性,因为 cookie 只能通过 https 协议发送),否则无效。 大概这样设置:Set-Cookie: CookieName=CookieValue; SameSite=Strict;

点击劫持

这个比较好理解,我们直接看例子: 其实就是通过覆盖不可见的页面,诱导用户点击而造成的攻击行为。稍微高级一点的伪装可以怎么样呢,就是做成一个游戏界面,让你在页面上狂点,而实际上呢,则会触发一些攻击事件,比如打开摄像头、发送邮件之类的。至于我们为什么会点击,因为骗子通常知道你想要什么😎,好奇心也驱使你这样做,并且我们当时并不觉得会有什么危险,所以我们都愿意试试看。

防御措施

禁止内嵌 iframe

让自己的网站不要被目标网站内嵌,可以通过 js 和设置头部 X-Frame-Options 实现:

  1. if (top.location !== window.location) top.location = window.location
  2. setHeader('X-Frame-Options', 'DENY')

验证码

增加攻击成本。

SQL 注入

因为这个和 XSS 有点小像,所以在这里稍微提及一下😯。
一句话解释:小王利用潜在的数据库漏洞访问或修改数据。

举个栗子

  1. 用户填写了用户名和密码,点击登录发送了一个请求。
  2. 后端接收到请求并解析参数,将其拼装成一个 SQL 语句执行,形如 select * from user where username = '${data.username}' and pwd='${data.pwd}',并返回登录成功。
  3. 结果小王在填写密码的时候写上了 1' or '1'='1,结果后端一拼接 SQL 语句就变成了 select * from user where username = 'xiaowang' and pwd = '1' or '1'='1',显然这是成立的,也会返回成功。

是不是和 XSS 的拼接漏洞很像呢,本来是数据的东西变成程序执行了。

防御措施

以下内容可跳过,仅作为了解:

  • 不要给出具体的错误信息,越具体越给了小王方向
  • 检查数据类型
  • 对数据进行转义
  • 使用参数化查询:相当于分成两条语句,第一步明确目的,不能再被修改;第二步不管传啥只当作数据处理
  • 使用 ORM(对象关系映射):就是可以不用拼 SQL 语句的意思

上传问题

一句话解释:上传的文件被当做程序解析执行。

举个栗子

  1. 小王上传了一个恶意文件 xx.php,内容如下:
<?php
    phpinfo();
?>
  1. 如果我们点击该文件的下载链接,就会执行该 php 文件(前提是服务器可以解析 php 文件,并且文件所在目录要有执行的权限)。

防御措施

  • 限制上传后缀 if (ext === 'js') throw new Error('xxx')
  • 文件类型检测 if (file.type !== 'images/png') throw new Error('xxx')
  • 检查文件内容以什么开头
  • 程序输出:就是不运行,有个读写的过程
  • 权限控制:可写可执行互斥原则

DoS 攻击

DoS(Denial of Service)攻击就是利用合理的服务请求来占用过多的服务资源,从而使得合法用户无法得到服务的响应。比如 SYNflood 攻击、IP 欺骗、带宽DoS攻击、塞满服务器的硬盘等攻击手段,这个作为了解就行,具体可自行百度。

其他相关措施

主要都是提高攻击门槛:

  • 明文变换成密文,虽然我们很多密码明文都是一样的,但不同网站的密文一般是不一样的
  • https 传输
  • 频率限制
  • 验证码

总结

没有绝对安全的网站,我们要做的就是提高攻击成本,问题的关键也不在于小王能不能破译,而是值不值得,毕竟付出与收获不成正比,谁愿意倒贴呢?🙌