目錄
示例
首先 我們通過如下示例來看一下SPA(單頁面應用)的CSRF攻擊
服務
cnpm i -g egg-init
egg-init --type=simple saas-server
cd saas-server && cnpm i
cnpm run dev
vim app/router.js
'use strict';
let count = 0;
module.exports = app => {
const { router } = app;
router.post('/login', async ctx => {
ctx.session.user = 'user';
ctx.body = { message: 'login success' };
ctx.status = 200;
});
router.post('/count', async (ctx, next) => {
if (!ctx.session.user) {
ctx.body = { message: 'need login' };
ctx.status = 403;
return;
}
await next();
}, async ctx => {
ctx.body = { message: 'count ' + count++ };
ctx.status = 200;
});
};
vim config/config.default.js
'use strict';
module.exports = appInfo => {
const config = exports = {};
config.keys = appInfo.name + '_1533543342201_7043';
config.middleware = [];
config.security = {
csrf: {
enable: false,
},
};
return config;
};
- 測試
curl -X POST localhost:7001/count # {"message":"need login"}
curl -c cookies -X POST localhost:7001/login # {"message":"login success"}
curl -b cookies -X POST localhost:7001/count # {"message":"count 0"}
跨域
cnpm i --save egg-cors
vim config/plugin.js
'use strict';
exports.cors = {
enable: true,
package: 'egg-cors',
};
vim config/config.default.js
'use strict';
module.exports = appInfo => {
const config = exports = {};
config.keys = appInfo.name + '_1533543342201_7043';
config.middleware = [];
config.security = {
csrf: {
enable: false,
},
};
config.cors = {
credentials: true,
origin: 'http://localhost:8080',
allowMethods: 'HEAD,OPTIONS,GET,PUT,POST,DELETE,PATCH',
};
return config;
};
前端
cnpm i -g @vue/cli
vue create saas-client
cd saas-client
yarn serve
vim src/App.vue
<template>
<div id="app">
<div>
<button v-on:click="login">登錄</button>
<a>{{message1}}</a>
</div>
<div>
<button v-on:click="count">計數</button>
<a>{{message2}}</a>
</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
message1: '',
message2: '',
}
},
methods: {
login() {
fetch('http://localhost:7001/login', { method: 'POST', credentials: 'include' })
.then(res => {
return res.json();
}).then(json => {
this.message1 = json;
}).catch(error => {
this.message1 = error;
});
},
count() {
fetch('http://localhost:7001/count', { method: 'POST', credentials: 'include' })
.then(res => {
return res.json();
}).then(json => {
this.message2 = json;
}).catch(error => {
this.message2 = error;
});
},
}
}
</script>
<style>
</style>
- 測試
使用瀏覽器打開http://localhost:8080
點擊"登錄"和"計數"按鈕后 效果如下
spa-csrf-01.png
攻擊
vue create csrf-client
cd csrf-client
yarn serve
vim src/App.vue
<template>
<div id="app">
<form action="http://localhost:7001/count" method="POST">
<input type="submit" value="點擊中大獎">
</form>
</div>
</template>
<script>
export default {
name: 'app',
}
</script>
<style>
</style>
- 測試
使用瀏覽器打開http://localhost:8081
點擊"點擊中大獎"按鈕后 效果如下
spa-csrf-02.png
spa-csrf-03.png
使用瀏覽器打開http://localhost:8080
點擊"計數"按鈕后 效果如下
spa-csrf-04.png
小結
通過上述示例 我們知道想要成功進行CSRF攻擊有如下兩個條件
條件1: 繞過瀏覽器跨域限制 例如: 上述<form>標簽 詳見Laravel框架 之 CSRF
條件2: 基于cookie存儲的session鑒權 AJAX請求會自動帶上cookie導致鑒權通過
對于前后端未分離的項目
條件1 無法回避
條件2 可以在<form>或<meta>添加隱藏的csrf-token來保證請求的有效性 詳見CSRF 保護
而對于前后端分離的項目
條件1: 同樣無法回避
條件2: 可以通過使用除cookie外的其他瀏覽器存儲 例如: sessionStorage或localStorage
因此 對于前后端分離的SPA應用 推薦使用基于非cookie存儲的token鑒權 詳見JWT入門 和 Laravel框架 之 Passport
問題
將token存儲于sessionStorage或localStorage中 會引起共享問題
例如 cookie的域名為".yourdomain.com" 那么"yourdomain.com"和"app.yourdomain.com"都可以訪問該cookie
但是 存儲于sessionStorage或localStorage中的token卻不能在不同域名(甚至subdomain)中共享
因此
- 可以將token存儲于域名為".yourdomain.com"的cookie中
并且
- 此時的cookie只做存儲而非鑒權 即服務端并不依賴request中的cookie進行權限校驗