[秃破前端面试] —— Web 安全相关

1,424 阅读10分钟

前言

年前年后跳槽季,准备从面试内容入手看看前端相关知识点,旨在探究一个系列知识点,能力范围之内的深入探究一下。重在实践,针对初级前端和准备面试的同学,争取附上实际的代码例子以及相关试题~系列名字就用【秃破前端】—— 因为圈内大家共识,技术与发量成正比。😄希望大家早日 破瓶颈~

关于面试题或者某个知识点的文章太多了,这里笔者只是想把个人的总结用代码仓库的形式记录下来并输出文章,毕竟理论不等于实践,知其然也要知其所以然,实践用过才能真正理解~

相关系列同类型文章:

同源策略

如果两个 URL 的协议域名端口都相同,则它们是同源的。浏览器存在同源限制,它是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

XSS(跨站脚本攻击 Cross Site Scripting)

XSS(Cross-Site Scripting,跨站脚本)是前端领域危害范围非常广的攻击方式。

Cross-Site Stripting 的缩写本应该是 CSS,但是为了避免和 CSS 概念缩写混淆,所以将Cross(即交叉)使用交叉形状的 X 表示。

从攻击时限分为 持久型非持久型

从攻击方式上分为 反射型存储型DOM-XSS

这里 DOM-XSS 我没太看出来和反射型 XSS 的具体区别,所以下面就把二者统称为非持久反射型 XSS 来对待了,因为表象很相似。

反射型 XSS 攻击 —— 非持久型

用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。 在现实生活中,黑客经常会通过 QQ 群或者邮件等渠道诱导用户去点击这些恶意链接。

直接来个 Demo 演示一下:

// 我们起个服务
app.get('/hello', (req, res) => {
  const { name } = req.query;
  res.send(`<h1>Hello, ${name}</h1>`);
});

/hello路径会接收一个 name 参数,然后会响应到页面上来展示。效果就是下面这样:

我们如果将参数写成一段恶意脚本,那么页面就会执行,嗯,可能你会觉得,不就是个弹窗吗,怎么就攻击了。但是我们得想一下,一个弹窗 = 一段 JS 代码,既然是 JS 代码,那么攻击者可以放一段脚本,比如获取你的cookie信息、账户信息等。

但是~这里我觉得有点问题,这该怎么定义呢?说实话,这也不算是纯前端安全啊,我的例子使用的是 Nodejs。实际上,也就是 PHP/JSP 这种语言经常出现的场景。比如我们再来看看如果是前端页面的跳转呢?

<script>
  function getUrlParam (name) {
    const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
    const r = window.location.search.substr(1).match(reg);
    if (r != null) return unescape(r[2]); return null;
  }
  window.onload = function() {
    const name = getUrlParam('name');
    document.getElementById('name').innerHTML = name;
  }
</script>

上面这段代码是纯前端获取url参数然后添加到页面:

可以看到,浏览器并不会执行,当然如果使用innerHTML会把字符串当成标签渲染成 html,而如果是innerText则是会当成字符串处理。

但是也是不会执行的。所以我们作为前端在讨论 XSS 的时候,其实多多少少需要夹带着后端相关。

攻击方向:浏览器(URL) -> 后台 -> 单个用户

非持久反射型 XSS 攻击,攻击方向是输入URL,后台接受攻击代码并把代码响应回了前端,这也就意味着,该攻击只对当前打开攻击链接的用户有效,页面关闭,攻击的影响就不存在了,所以是非持久的。

存储型 XSS 攻击

利用漏洞提交恶意的JavaScript代码,比如在inputtextare区域内填写一段脚本代码<script src="http://恶意网站脚本"></script>,当用户存储然后再打开预览页面的时候,该段脚本就会执行,然后将用户相关的信息(如cookie等)上传到对方服务器。

攻击方向:浏览器 -> 后台 -> 数据库 -> 后台 -> 用户(所有使用者)

持久存储型 XSS 攻击,该攻击将攻击代码存储到数据库,页面关闭,如果没有任何安全措施,那么该系统所有的使用者,只要进入到攻击页面,都会受到攻击,数据不消失,攻击就是永久有效的。

一个最简单的例子,就是表单存储了。比如,一个博客系统,攻击者输入恶意评论,那么该文章只要被展示,该评论就会被渲染攻击正在访问的用户。为了进行简单的代码演示,这里我的 Demo 写的比较粗糙,毕竟不是所有场景都适合假想代码的,很多都必须实际应用中才行。

  • 存储型需要存入数据库

我这里后端就用文件流去读写db.txt文件假设就是数据库了。

/**
 * 用户评论存入 db.txt
 */
app.post('/comment', (req, res) => {
  fs.writeFile(
    path.resolve(__dirname, './db.txt'),
    `${req.body.comment}\n`,
    { flag: 'a', encoding: 'utf-8' },
    (err) => {
      if (err) console.log(err);
      res.status(200).json({
        message: 'success'
      });
    });
});
  • 前端数据录入
async function postComment() {
    const comment = document.getElementById('comment').value;
    const data = {
      author: 'luffy',
      comment
    };
    const res = await fetch('http://127.0.0.1:3000/comment', {
      method: 'post',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    const resData = await res.json();
    // do something
  }

最后效果就如下图:

可以看到,为什么存储型是持久性的,上面的代码运行过后,攻击代码存入数据库,不论何人何地何时访问这个链接,都会执行攻击脚本。

预防策略

  • 将cookie等敏感信息设置为httpOnly,禁止Javascript通过document.cookie获得。

  • 对所有的输入做严格的校验尤其是在服务器端,过滤掉任何不合法的输入,比如手机号必须是数字。

  • 净化和过滤掉不必要的 html 标签,比如:<iframe>, alt, <script> ;净化和过滤掉不必要的Javascript的事件标签,比如:onclick, onfocus等。

  • 转义单引号,双引号,尖括号等特殊字符,可以采用html encode编码或者过滤掉这些特殊字符。

也就是一般来说前后端都要做校验,前端校验的是输入合法性,后端校验的是安全性相关,上面也提到了,事实上,前端做不做都不会有什么影响,恶意脚本代码并不会执行,这个事应该是后端(Java、Node.js、php等)去做的。

跨站请求伪造(CRSF Cross Site Request Forgery)

引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。 发起 CSRF 攻击的三个必要条件:

  • 目标站点一定要有 CSRF 漏洞;
  • 用户要登录过目标站点,并且在浏览器上保持有该站点的登录状态;
  • 需要用户打开一个第三方站点,如黑客的站点等。

CRSF 的要求比较多,既需要用户登录状态下打开攻击站点,又需要攻击站点对用户登录的网站相关内容有详细的了解(包括 API 及参数),所以不是很好演示代码,这里就不模拟场景了。

比如,在出现CSRF漏洞的博客网站,http://blog.com?postId=111&delete=1表示删除id=111的用户文章,当然这个条件是用户在该网站已经登录,然后攻击者新建一个页面,页面内容是一个图片<img src='http://blog.com?postId=111&delete=1' />。当该用户打开页面的时候,就执行了这个删除操作,防不胜防,还有更致命的就是银行账户转账的操作。

预防策略

验证请求的来源 - 很重要

如果是敏感请求,可以判断一下请求的OriginReferer。Referer 是 HTTP 请求头中的一个字段,记录了该 HTTP 请求的来源地址,而O rigin 属性只包含了域名信息,并没有包含具体的 URL 路径。这是 Origin 和 Referer 的一个主要区别。 服务器的策略是优先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际情况判断是否使用 Referer 值。

利用好 Cookie 的SameSite属性

SameSite 选项通常有 Strict、Lax 和 None 三个值。

  • SameSite 的值是 Strict,那么浏览器会完全禁止第三方 Cookie。

  • Lax 相对宽松一点。在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法,或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。

  • 如果使用 None 的话,在任何情况下都会发送 Cookie 数据

使用token验证

所有请求的身份信息判断使用 token 来验证。具体为什么使用 token,大家可以去查看 token 的工作原理。

其他安全相关场景

感觉面试经常问的也就是 XSS 和 CSRF 这两个,至于下面这些,就不写 Demo 了,简单了解一下就可以了,如果面试官臭不要脸的问了,咱们就谦虚谦虚,说不知道顺便请教一下,也就可以了😄。

点击劫持

  • 诱使用户点击看似无害的按钮(实则点击了透明 iframe 中的按钮).
  • 监听鼠标移动事件,让危险按钮始终在鼠标下方.
  • 使用 HTML5 拖拽技术执行敏感操作(例如 deploy key).

点击劫持这件事一般来说敏感操作使用验证码是最简单的解决办法。

预防策略:

  • 服务端添加 X-Frame-Options 响应头,这个 HTTP 响应头是为了防御用 iframe 嵌套的点击劫持攻击,这样浏览器就会阻止嵌入网页的渲染。
  • JS 判断顶层视口的域名是不是和本页面的域名一致,不一致则不允许操作,top.location.hostname === self.location.hostname
  • 敏感操作使用更复杂的步骤(验证码、输入项目名称以删除)。

window.opener 安全问题

window.opener 表示打开当前窗体页面的的父窗体的是谁。例如,在 A 页面中,通过一个带有 target="_blank" 的 a 标签打开了一个新的页面 B,那么在 B 页面里,window.opener 的值为 A 页面的 window 对象。 一般来说,打开同源(域名相同)的页面,不会有什么问题。但对于跨域的外部链接来说,存在一个被钓鱼的风险。比如你正在浏览购物网站,从当前网页打开了某个外部链接,在打开的外部页面,可以通过 window.opener.location 改写来源站点的地址。利用这一点,将来源站点改写到钓鱼站点页面上,例如跳转到伪造的高仿购物页面,当再回到购物页面的时候,是很难发现购物网站的地址已经被修改了的,这个时候你的账号就存在被钓鱼的可能了。 预防策略:

解决办法设置 rel 属性

<a href="https://xxxx" rel="noopener noreferrer"> 外链 <a>
  • rel="noopener noreferrer" 规定禁止新页面传递源页面的地址,通过设置了此属性的链接打开的页面,其 window.opener 的值为 null。

  • 将外链替换为内部的跳转连接服务,跳转时先跳到内部地址,再由服务器 redirect 到外链。

  • 可以由widow.open打开外链。

相关题目

1. 什么是 XSS 攻击?

2. 什么是 CSRF 攻击?

3. 如何预防前端攻击?

总结

写到这里,说实话从面试角度来说了解掌握即可,接触的也都不是很深,但是总结起来感觉就是 —— 安全问题发生在前端(无论是 url 还是 form 表单),而处理办法则是在后台

Web 安全相关的知识大家可能实际使用编写中并不会在意很多,一般也都是出现问题,或者公司的安全部发现问题提出来了,才会想着去解决。实际上,Web 安全对于前端开发来说确实十分重要,现在回头看以前的代码,感后脊骨发凉,好像并不是很安全啊,赶紧回去加固加固~😄

秃破系列代码地址