从 0 到 1 讲解如何从前后端有效防范点击劫持攻击
(给大前端技术之路加星标,提升前端技能)
导语:点击劫持攻击可以在视觉上欺骗用户点击当前页面元素实际点击另一个网站的元素。那么点击劫持攻击是如何在视觉上欺骗获取用户进行点击的呢?我们又如何在客户端和服务端采取有效措施来防止点击劫持攻击呢?
点击劫持攻击的定义
点击劫持攻击
就是—欺骗当前网站的用户在不知情的情况下在目标网站点击。例如,用户在当前网站看到一条中奖信息,当用户点击中奖信息按钮时,实际是在点击购物网站的购买按钮。通常此类攻击是通过隐藏目标网站的界面或者对界面进行重新排列,从而使用户并不知道是在点击目标网站,因此点击劫持攻击又称为UI覆盖攻击
。
由于目标网站的伪装欺骗,用户会在不知不觉中进行转账、购物、社交网站点赞等操作。
点击劫持攻击的类型
根据点击劫持的目的,可以分为以下几种类型:
1.点赞劫持
:这类点击劫持的目的是获取用户的点击并重定向到Facebook或其他社交网站的页面去点赞。
2.Cookie劫持
:这类攻击会引导用户进行拖拽操作,并使用户存储cookie在浏览器中。这样攻击者可以获取到用户cookie从而在目标网站上假冒用户进行操作。
3.文件劫持
:攻击者可以访问用户的本地文件并进行读写操作。
4.光标劫持
:这类攻击在用户不知情的情况下改变光标的实际位置,因此用户以为在执行A操作,实际在执行B操作。
5.密码管理器劫持
:这类攻击在自动填充密码操作欺骗密码管理器获取用户密码。
以上是点击劫持具体分类中的几种。点击劫持可以分为很多种类,但是所有的点击劫持的原理都是相同的:通过视觉欺骗用户从而获取用户的点击或拖拽等操作。
点击劫持攻击的过程
为了更好的理解点击劫持攻击,让我们看看在实际攻击产生过程中具体发生了什么。这将帮助我们更好的理解攻击者的行为并作出相应的防御措施。
运行本文的示例,需要Node.js运行环境,但是点击劫持攻击的原理和防御与具体的编程语言和框架并无必要关系。
1.搭建环境
首先,下载本文的示例程序,命令如下:
git clone https://github.com/auth0-blog/clickjacking-sample-app.git
其次,下载完成后打开clickjacking-sample-app文件夹,安装项目所需要的依赖,命令如下:
npm install
再次,启动程序,命令如下:
npm start
最后,打开浏览器,导航栏中输入 http://localhost:3000,网页效果如下:
上图是电影网站中的某个具体电影介绍,在当前网页可以观看电影和购买DVD。实际项目中需要进行session认证才能观看或者购买,简便起见,本项目并未实现实际的认证过程。我们可以通过点击页面底部的valid session,进行模拟session认证,获取到模拟的用户seesion后,这句话将消失。
2.发起点击劫持攻击
电影网站运行后,我们将要对其进行点击劫持。这时还需要运行另一个网站—攻击网站,它将抓取用户的点击,并在用户不知不觉的情况下重定向到电影网站。下一步让我们启动攻击网站,在终端中输入以下命令:
node attacker-server.js
然后,在浏览器中打开新的窗口,在地址栏中输入http://localhost:4000。你将看到如下页面:
上图的网页为获奖消息通知,点击“Accept the prize!”按钮即可领奖。这看起来完全和电影网站无关,然而如果点击领取奖励按钮将会在电影网站购买DVD。点击领奖按钮前,我们打开Chrome浏览器的开发者工具栏的Network来分析发送的网络请求。如下图所示:
我们可以看到上图是发送到http://localhost:3000/purchase 也就是电影网站的POST请求,领奖网站和电影网站之间有什么关系呢?想想文章开始我们提到的点击劫持攻击的原理:电击劫持攻击就是通过视觉欺骗获取用户的点击,从当前网站引诱用户到攻击网站。
点击劫持攻击的分析
为了更好的理解点击劫持攻击,我们看一下攻击网站的源码,其中views/index.ejs的代码如下:
<!-- views/index.ejs -->
<html lang=en>
<!-- ...已有代码... -->
<body>
<div id="attacker_website">
<h1>You Are The Winner!!!</h1>
<h2>You won an awesome tropics holiday!</h2>
<img alt="Tropical Holiday" src="images/tropical-holiday.jpg">
<h2>Accept it by clicking the button below.</h2>
<button type="submit">Accept the prize!</button>
</div>
<iframe id="vulnerable_website" src="http://localhost:3000">
</iframe>
</body>
</html>
其中ejs模版中主要有以下两个元素:1.可见部分:id="attacker_website"的div元素 2.不可见部分:id="vulnerable_website"的iframe元素,在攻击者网站上我们并不能看到iframe的内容 这是因为CSS规则定义了元素的位置及可见性,如下所示:
<!-- views/index.ejs -->
<html lang=en>
<head>
<!-- ...已有代码... -->
<style>
#vulnerable_website {
position:relative;
opacity:0.0;
width:800px;
height:900px;
top:75px;
left: -95px;
z-index:2;
padding-left:80px;
}
#attacker_website {
position:absolute;
z-index:1;
}
#attacker_website button {
margin-left:100px;
}
</style>
</head>
<!-- ...已有代码... -->
</html>
其中,id="vulnerable_website"的iframe元素的opacity为0.0,这代表iframe的内容完全透明。而且iframe的位置和id="attacker_website"的div元素的位置重叠。我们把透明度opacity设为0.3,然后重启攻击者网站,效果如下:
我们可以看到“Accept the prize!”按钮和“Buy DVD”按钮是重叠的。其中z-index的作用是,将iframe元素置于攻击者网站的上层,因此当用户点击“Accept the prize!”时,其实是点击“Buy DVD”,所以点击劫持攻击叫做UI覆盖攻击。
点击劫持攻击与CSRF攻击的不同
现在可能觉得点击劫持和CSRF攻击有些类似,都是攻击者通过当前网站向目标网站发送请求。
他们的最大不同点在于攻击者是否构造请求,CSRF是攻击者直接构造HTTP请求向目标网站发送,而点击劫持是直接访问目标网站。
关于CSRF想了解更多可以参考:https://auth0.com/blog/cross-site-request-forgery-csrf/
点击劫持攻击的防御
以上的例子用到了传统的iframe和css,这也意味着它不受前端框架的限制可以用在任何前端项目中,可以对React、Vue、Angular以及服务器端造成影响。
1.frame busting
因为点击劫持是通过iframe进行的,因此我们可以考虑如何在客户端禁止iframe加载。几行代码即可以实现禁止iframe加载,称为frame busting。
在被攻击网站templates/index.ejs中加入以下代码:
<!-- templates/index.ejs -->
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" type="text/css" href="css/tacit-css.min.css"/>
<title>The Vulnerable Movie Center</title>
<!-- 👇 新加入代码 -->
<script>
if (top != window) {
top.location = window.location;
}
</script>
<!-- ...已有代码... -->
</html>
如果被攻击网站的最顶层页面并不是window,则给最顶层页面重新赋值window重新加载。实际上是使用当前的页面替代攻击网站,从而躲避攻击。
这个解决办法看起来不错,然而还存在一些问题。例如,一些浏览器或者浏览器插件会阻止重新加载,攻击者会重构攻击页面以突破防护措施。可通过以下代码突破防御措施:
<!-- views/index.ejs -->
<html lang=en>
<head>
<!-- ...已有代码... -->
</head>
<body>
<!-- 👇 新加入代码 -->
<script>
window.onbeforeunload = function() {
return false;
};
</script>
<!-- ...已有代码... -->
</body>
</html>
当检测到onbeforeunload事件时,也就是重新加载前,攻击者直接返回fale并不重新加载,这样就不能使用当前的页面替代攻击网站。但这个方法在某些浏览器中无法使用,因为这需要用户的确认操作。但攻击者还有其他措施,例如使用iframe的sandbox属性,如下所示:
<!-- views/index.ejs -->
<html lang=en>
<head>
<!-- ...已有代码... -->
</head>
<body>
<!-- ...已有代码... -->
<!-- 👇 新加入代码 -->
<iframe id="vulnerable_website"
src="http://localhost:3000"
sandbox="allow-scripts allow-forms allow-same-origin">
</iframe>
</body>
</html>
以上的例子中,攻击者通过sandbox的allow-scripts、allow-forms、allow-same-origin表示允许执行脚本、提交表单、将iframe父元素内容当作同源。
从以上两个例子可以看出,frame busting的点击劫持防御很容易被绕过,因此除了防御旧版浏览器的点击劫持攻击并不推荐以上的解决办法。
除了以上办法,还有很多基于JavaScript和HTML的解决办法,但都效果不佳。
frame busting的完整代码如下:
git clone -b client-side-defenses https://github.com/auth0-blog/clickjacking-sample-app.git
2.X-Frame-Options
防御点击劫持的更好办法是,在HTTP头中加入X-Frame-Options,禁止frame加载。
在server.js中加入以下代码:
// server.js
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const port = 3000;
const app = express();
app.set('views', './templates');
app.set('view engine', 'ejs');
//👇 新加入代码
app.use(function(req, res, next) {
res.setHeader('X-Frame-Options', 'sameorigin');
next();
});
// ...已有代码...
app.listen(port, () => console.log(`The server is listening at http://localhost:${port}`));
我们可以看到,在Express中间件请求头中加入了X-Frame-Options,他的值为sameorigin,表示只有同源域名的frame可以加载。X-Frame-Options值还可以为deny,表示任何来源的frame都不能被加载。
这样,攻击者将不能使用上面的措施绕过防御,让我们再将views/index.ejs中的opacity改为0.3,重新启动项目,试下防御措施是否生效,可以看到iframe内容被禁止加载了,如下图:
X-Frame-Options的完整代码地址如下:
git clone -b x-frame-options https://github.com/auth0-blog/clickjacking-sample-app.git
3.CSP
大多数网站都支持X-Frame-Options方式,但是这并未被标准化,所以还是有些网站并不支持此种方式。还可以采用另一种标准化方法内容安全策略(CSP)。
在原始的server.js中加入以下代码:
// server.js
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const port = 3000;
const app = express();
app.set('views', './templates');
app.set('view engine', 'ejs');
//👇 新加入代码
app.use(function(req, res, next) {
res.setHeader("Content-Security-Policy", "frame-ancestors 'self';");
next();
});
// ...已有代码...
app.listen(port, () => console.log(`The server is listening at http://localhost:${port}`));
在本例中,每个发出的请求头中Content-Security-Policy的值为frame-ancestors 'self',这表示只有符合同源策略的frame才可以加载。
Content-Security-Policy的值还可以如下:
frame-ancestors 'none',表示任何来源的frame都不可以加载。 rame-ancestors 'https://www.authorized-website.com;',表示来源为https://www.authorized-website.com的frame可以加载,可以指定多个网址。
CSP的完整代码地址如下:
git clone -b x-frame-options https://github.com/auth0-blog/clickjacking-sample-app.git
4.cookie的同源策略
如果是类似本文的采用了cookie会话的项目,则可以通过限制cookie只在同源下。这种措施并没有阻止iframe的加载,而是使得会话在iframe中的会话无效。这是一项新技术,可能旧版本的浏览器并不支持。
在原始的server.js文件中加入以下代码:
// server.js
// ...已有代码...
app.use(express.static('public'));
app.use(session({
secret: 'my-secret',
resave: true,
saveUninitialized: true,
cookie: {
httpOnly: true,
sameSite: 'strict' //👈 新加入的代码
}
}));
app.use(bodyParser.urlencoded({ extended: true }));
// ...已有代码...
app.listen(port, () => console.log(`The server is listening at http://localhost:${port}`));
其中sameSite属性为strict,表示不符合同源策略时请求中不能包含cookie。
same-site-cookie的完整代码地址如下:
git clone -b same-site-cookie https://github.com/auth0-blog/clickjacking-sample-app.git
总结
本文介绍了点击劫持攻击的原理、点击劫持的各种防御方法以及各种方法的优缺点。最好的防御措施是把这些方法结合起来,这样在一种方法失败的情况(例如浏览器不支持)下,另一种方法可能会生效。
- EOF -
觉得本文对你有帮助?请分享给更多人
关注「大前端技术之路」加星标,提升前端技能
点赞和在看就是最大的支持❤️