文章首次發表在 我的博客html
以前寫過一篇 web安全之XSS實例解析,是經過舉的幾個簡單例子講解的,一樣經過簡單得例子來理解和學習CSRF,有小夥伴問實際開發中有沒有遇到過XSS和CSRF,答案是有遇到過,不過被測試同窗發現了,還有安全掃描發現了可能的問題,這兩篇文章就是簡化了一下當時實際遇到的問題。前端
跨站請求僞造(Cross Site Request Forgery),是指黑客誘導用戶打開黑客的網站,在黑客的網站中,利用用戶的登錄狀態發起的跨站請求。CSRF攻擊就是利用了用戶的登錄狀態,並經過第三方的站點來作一個壞事。node
要完成一次CSRF攻擊,受害者依次完成兩個步驟:git
在a.com
登錄後種下cookie, 而後有個支付的頁面,支付頁面有個誘導點擊的按鈕或者圖片,第三方網站域名爲 b.com
,中的頁面請求 a.com
的接口,b.com
其實拿不到cookie,請求 a.com
會把Cookie自動帶上(由於Cookie種在 a.com
域下)。這就是爲何在服務端要判斷請求的來源,及限制跨域(只容許信任的域名訪問),而後除了這些還有一些方法來防止 CSRF 攻擊,下面會經過幾個簡單的例子來詳細介紹 CSRF 攻擊的表現及如何防護。github
下面會經過一個例子來說解 CSRF 攻擊的表現是什麼樣子的。
實現的例子:
在先後端同域的狀況下,先後端的域名都爲 http://127.0.0.1:3200
, 第三方網站的域名爲 http://127.0.0.1:3100
,釣魚網站頁面爲 http://127.0.0.1:3100/bad.html
。web
平時本身寫例子中會用到下面這兩個工具,很是方便好用:
前端頁面: client.htmljson
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>CSRF-demo</title> <style> .wrap { height: 500px; width: 300px; border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; } input { width: 300px; } .payInfo { display: none; } .money { font-size: 16px; } </style> </head> <body> <div class="wrap"> <div class="loginInfo"> <h3>登錄</h3> <input type="text" placeholder="用戶名" class="userName"> <br> <input type="password" placeholder="密碼" class="password"> <br> <br> <button class="btn">登錄</button> </div> <div class="payInfo"> <h3>轉帳信息</h3> <p >當前帳戶餘額爲 <span class="money">0</span>元</p> <!-- <input type="text" placeholder="收款方" class="account"> --> <button class="pay">支付10元</button> <br> <br> <a href="http://127.0.0.1:3100/bad.html" target="_blank"> 據說點擊這個連接的人都賺大錢了,你還不來看一下麼 </a> </div> </div> </body> <script> const btn = document.querySelector('.btn'); const loginInfo = document.querySelector('.loginInfo'); const payInfo = document.querySelector('.payInfo'); const money = document.querySelector('.money'); let currentName = ''; // 第一次進入判斷是否已經登錄 Fetch('http://127.0.0.1:3200/isLogin', 'POST', {}) .then((res) => { if(res.data) { payInfo.style.display = "block" loginInfo.style.display = 'none'; Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 0}) .then((res) => { money.innerHTML = res.data.money; }) } else { payInfo.style.display = "none" loginInfo.style.display = 'block'; } }) // 點擊登錄 btn.onclick = function () { var userName = document.querySelector('.userName').value; currentName = userName; var password = document.querySelector('.password').value; Fetch('http://127.0.0.1:3200/login', 'POST', {userName, password}) .then((res) => { payInfo.style.display = "block"; loginInfo.style.display = 'none'; money.innerHTML = res.data.money; }) } // 點擊支付10元 const pay = document.querySelector('.pay'); pay.onclick = function () { Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 10}) .then((res) => { console.log(res); money.innerHTML = res.data.money; }) } // 封裝的請求方法 function Fetch(url, method = 'POST', data) { return new Promise((resolve, reject) => { let options = {}; if (method !== 'GET') { options = { headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), } } fetch(url, { mode: 'cors', // no-cors, cors, *same-origin method, ...options, credentials: 'include', }).then((res) => { return res.json(); }).then(res => { resolve(res); }).catch(err => { reject(err); }); }) } </script> </html>
實現一個簡單的支付功能:segmentfault
後端服務: server.js後端
const Koa = require("koa"); const app = new Koa(); const route = require('koa-route'); const bodyParser = require('koa-bodyparser'); const cors = require('@koa/cors'); const KoaStatic = require('koa-static'); let currentUserName = ''; // 使用 koa-static 使得先後端都在同一個服務下 app.use(KoaStatic(__dirname)); app.use(bodyParser()); // 處理post請求的參數 // 初始金額爲 1000 let money = 1000; // 調用登錄的接口 const login = ctx => { const req = ctx.request.body; const userName = req.userName; currentUserName = userName; // 簡單設置一個cookie ctx.cookies.set( 'name', userName, { domain: '127.0.0.1', // 寫cookie所在的域名 path: '/', // 寫cookie所在的路徑 maxAge: 10 * 60 * 1000, // cookie有效時長 expires: new Date('2021-02-15'), // cookie失效時間 overwrite: false, // 是否容許重寫 SameSite: 'None', } ) ctx.response.body = { data: { money, }, msg: '登錄成功' }; } // 調用支付的接口 const pay = ctx => { if(ctx.method === 'GET') { money = money - Number(ctx.request.query.money); } else { money = money - Number(ctx.request.body.money); } ctx.set('Access-Control-Allow-Credentials', 'true'); // 根據有沒有 cookie 來簡單判斷是否登陸 if(ctx.cookies.get('name')){ ctx.response.body = { data: { money: money, }, msg: '支付成功' }; }else{ ctx.body = '未登陸'; } } // 判斷是否登錄 const isLogin = ctx => { ctx.set('Access-Control-Allow-Credentials', 'true'); if(ctx.cookies.get('name')){ ctx.response.body = { data: true, msg: '登錄成功' }; }else{ ctx.response.body = { data: false, msg: '未登陸' }; } } // 處理 options 請求 app.use((ctx, next)=> { const headers = ctx.request.headers; if(ctx.method === 'OPTIONS') { ctx.set('Access-Control-Allow-Origin', headers.origin); ctx.set('Access-Control-Allow-Headers', 'Content-Type'); ctx.set('Access-Control-Allow-Credentials', 'true'); ctx.status = 204; } else { next(); } }) app.use(cors()); app.use(route.post('/login', login)); app.use(route.post('/pay', pay)); app.use(route.get('/pay', pay)); app.use(route.post('/isLogin', isLogin)); app.listen(3200, () => { console.log('啓動成功'); });
執行 nodemon server.js
,訪問頁面 http://127.0.0.1:3200/client.html
跨域
登錄完成以後,能夠看到Cookie是種到 http://127.0.0.1:3200
這個域下面的。
第三方頁面 bad.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>第三方網站</title> </head> <body> <div> 哈哈,小樣兒,哪有賺大錢的方法,仍是踏實努力工做吧! <!-- form 表單的提交會伴隨着跳轉到action中指定 的url 連接,爲了阻止這一行爲,能夠經過設置一個隱藏的iframe 頁面,並將form 的target 屬性指向這個iframe,當前頁面iframe則不會刷新頁面 --> <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr" style="display: none"> <input type="text" name="userName" value="xiaoming"> <input type="text" name="money" value="100"> </form> <iframe name="targetIfr" style="display:none"></iframe> </div> </body> <script> document.querySelector('.form').submit(); </script> </html>
使用 HTTP-server 起一個 本地端口爲 3100的服務,就能夠經過 http://127.0.0.1:3100/bad.html
這個連接來訪問,CSRF攻擊須要作的就是在正常的頁面上誘導用戶點擊連接進入這個頁面
點擊誘導連接,跳轉到第三方的頁面,第三方頁面自動發了一個扣款的請求,因此在回到正常頁面的時候,刷新,發現錢變少了。
咱們能夠看到在第三方頁面調用 http://127.0.0.1:3200/pay
這個接口的時候,Cookie自動加在了請求頭上,這就是爲何 http://127.0.0.1:3100/bad.html
這個頁面拿不到 Cookie,可是卻能正常請求 http://127.0.0.1:3200/pay
這個接口的緣由。
CSRF攻擊大體能夠分爲三種狀況,自動發起Get請求, 自動發起POST請求,引導用戶點擊連接。下面會分別對上面例子進行簡單的改造來講明這三種狀況
在上面的 bad.html中,咱們把代碼改爲下面這樣
<!DOCTYPE html> <html> <body> <img src="http://127.0.0.1:3200/payMoney?money=1000"> </body> </html>
當用戶訪問含有這個img的頁面後,瀏覽器會自動向自動發起 img 的資源請求,若是服務器沒有對該請求作判斷的話,那麼會認爲這是一個正常的連接。
上面例子中演示的就是這種狀況。
<body> <div> 哈哈,小樣兒,哪有賺大錢的方法,仍是踏實努力工做吧! <!-- form 表單的提交會伴隨着跳轉到action中指定 的url 連接,爲了阻止這一行爲,能夠經過設置一個隱藏的iframe 頁面,並將form 的target 屬性指向這個iframe,當前頁面iframe則不會刷新頁面 --> <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr"> <input type="text" name="userName" value="xiaoming"> <input type="text" name="money" value="100"> </form> <iframe name="targetIfr" style="display:none"></iframe> </div> </body> <script> document.querySelector('.form').submit(); </script>
上面這段代碼中構建了一個隱藏的表單,表單的內容就是自動發起支付的接口請求。當用戶打開該頁面時,這個表單會被自動執行提交。當表單被提交以後,服務器就會執行轉帳操做。所以使用構建自動提交表單這種方式,就能夠自動實現跨站點 POST 數據提交。
誘惑用戶點擊連接跳轉到黑客本身的網站,示例代碼如圖所示
<a href="http://127.0.0.1:3100/bad.html">據說點擊這個連接的人都賺大錢了,你還不來看一下麼</a>
用戶點擊這個地址就會跳到黑客的網站,黑客的網站可能會自動發送一些請求,好比上面提到的自動發起Get或Post請求。
SameSite有3個值: Strict, Lax和None
咱們能夠根據實際狀況將一些關鍵的Cookie設置 Stirct或者 Lax模式,這樣在跨站點請求的時候,這些關鍵的Cookie就不會被髮送到服務器,從而使得CSRF攻擊失敗。
因爲CSRF攻擊大多來自第三方站點,能夠在服務器端驗證請求來源的站點,禁止第三方站點的請求。
能夠經過HTTP請求頭中的 Referer和Origin屬性。
可是這種 Referer和Origin屬性是能夠被僞造的,碰上黑客高手,這種判斷就是不安全的了。
CSRF Token
(在request中要帶上以前獲取到的Token,好比 x-csrf-token:xxxx
), 而後服務器會驗證該Token是否合法。第三方網站發出去的請求是沒法獲取到 CSRF Token
的值的。Cookie是種在服務端的域名下的,好比客戶端域名是 a.com,服務端的域名是 b.com, Cookie是種在 b.com域名下的,在 Chrome的 Application下是看到的是 a.com下面的Cookie,是沒有的,以後,在a.com下發送b.com的接口請求會自動帶上Cookie(由於Cookie是種在b.com下的)
複雜請求須要處理option請求。
以前寫過一篇特別詳細的文章 CORS原理及@koa/cors源碼解析,有空能夠看一下。
若是沒有配置credential 這個參數,fetch是不會發送Cookie的
credential的參數以下
日常寫一些簡單的例子,從不少細節問題上也能補充本身的一些知識盲點。