淺析常見的 Web 安全預防

1. CSRF

跨站請求偽造,英文名:Cross-site request forgery

CSRF 攻擊流程

結合下面這張圖說明 CSRF 的攻擊流程。

李四在網站 A 註冊了用戶名,並且登錄到了網站 A,在登錄之後網站 A,就會給用戶李四的瀏覽器下發一個 cookie,李四在訪問網站 A 的時候,就會將本地的 cookie 上傳到網站 A,如果有這個 cookie 就判斷李四處於登錄狀態,如果沒有就判斷李四處於未登錄狀態。

現在李四處於登錄狀態,再訪問網站 A 的時候,cookie 會被上傳到 網站 A,因此網站 A 會認定 李四 處於登錄狀態。

現在李四又去訪問網站 B,網站 B 的頁面上放了一個在網站 A 上成為張三粉絲的鏈接,當李四點擊這個鏈接的時候,就會去訪問網站 A,這個時候網站 A 經驗證發現要李四處於登錄狀態,並且請求要成為張三的粉絲,合情合法,因此,李四在不知情的條件下就成為了張三的粉絲。

成為粉絲還是小事,如果是你設置了某個免密支付網站的付款鏈接呢,所以不要輕易去點擊不可信的鏈接。

一句話總結 CSRF 就是,利用你的身份在你不知情的情況下去做一些事情。

CSRF 的特點

  • 攻擊一般發起在第三方網站(網站 A),而不是被攻擊的網站(網站 B),被攻擊的網站(網站 A)無法防止攻擊發生,只能去防禦。

  • 攻擊利用受害者(李四)在被攻擊網站(網站 A)的登錄憑證,冒充受害者(李四)發出請求,而不是直接竊取數據。

  • 整個過程攻擊者(張三)並不能獲取到受害者(李四)的登錄憑證(cookie 等信息),僅僅是「冒用」。

    這一點很重要,由於瀏覽器的同源策略,那麼攻擊者只是去使用受害者的 cookie 信息,並不能獲取到 cookie。

  • 跨站請求可以用各種方式:圖片URL、超鏈接、CORS、Form提交等等,部分請求方式可以直接嵌入在第三方論壇、文章中,難以進行追蹤。

CSRF 的攻擊類型

1)GET 類型的 CSRF

GET 類型的 CSRF 攻擊只需要一個鏈接就可以了,例如用戶訪問了一個鏈接,這個鏈接下的 html 文件中有下面這個 img 標籤,這個時候會自動向 img 標籤 src 指向的惡意鏈接發出請求。

<img alt='#' src='惡意鏈接' />

2)POST類型的 CSRF

POST 類型的 CSRF 可以通過在頁面中嵌入一個表單來實現。

<form action='惡意鏈接' method='POST' >
  <input type="hidden" name="account" value="codingOrange" /> 
</form>
<script>document.forms[0].submit()</script>

document.forms 表示獲取到頁面中所有的 form 表單元素,document.forms[0] 表示獲取到頁面中第一個表單元素,submit() 表示提交表單

CSRF 防禦措施

1)盡量使用 POST 請求

通過上面的描述可以發現,使用 GET 請求更容易偽造鏈接,但是其實都差不多,不能依靠這種方式來防範 CSRF 攻擊。

2)使用驗證碼

對某些請求需要人為地加入驗證碼,確保這次的請求時用戶許可的行為。例如在付款時,需要用戶再次輸入密碼,在發起某個請求時,需要用戶手動輸入驗證等。

3)驗證來源站點

既然 CSRF 攻擊一般是從另一個網站發起的,那麼服務端可以通過 http header 中的 referer,orgin 驗證這次請求的來源站點,如果不是本站點那麼就可以將這次請求攔截。

4)CSRF Token

在用戶向服務端第一次請求頁面時,服務端會返回一個 Token,前端拿到這個 Token 之後,可以將所有的(通過遍歷 DOM 元素)能發出請求(a ,img,form 等標籤)的鏈接上都加上這個 Token 信息,CSRF 攻擊者並不能獲取到 Token(瀏覽器的同源策略),所以也就不能偽造出惡意請求鏈接,偽造出的惡意鏈接中沒有 Token 信息,服務端就能分辨出這不是用戶的請求。

舉個例子吧:

在網站 A 上,正常點擊時發出的請求的鏈接是

//www.a.com/?token=tokenMessage

CSRF 攻擊者由於獲取不到 tokenMessage,所以並不能偽造出上面能正常發出請求的鏈接。

5)雙重 cookie

利用瀏覽器的同源策略,攻擊者是不能獲取到 cookie 信息,因此在用戶第一次訪問時,服務端可以在 setCookie 中寫入一個 CSRFMessage 信息,因此前端通過解析 cookie 就可以獲取到這個 CSRFMessage,然後在發送的所有請求的 url 中都加上這個 CSRFMessage,服務端在接收到請求後通過請求中是否有 CSRFMessage 信息來判斷是不是用戶的請求。

2. XSS

下面只是說了很少的一部分,更多內容請參考 前端安全系列(一):如何防止XSS攻擊?

XSS(cross-site scriping)跨站腳本攻擊。

XSS 攻擊原理

向頁面注入腳本。例如在評論區寫評論時,寫上下面這段評論

<script>
	惡意代碼
</script>

也可以通過某個標籤綁定事件等方式向頁面中注入 JS 代碼。

XSS 防禦措施

使用 xss 庫中的 xss 函數,在 react,vue,node 中都可以使用。

# 安裝 xss
npm install xss --save

使用 xss

import xss from 'xss'
xss('<script>惡意代碼</script>')

CSRF 和 XSS 的區別

XSS 是通過在頁面注入 JS 代碼(惡意代碼)來攻擊

CSRF 是通過借用別人的身份去做一些事。

3. sql 注入

  • 最原始、最簡單的攻擊,自從有了 web2.0 就有了 sql 注入攻擊

  • 攻擊方式:輸入一個 sql 片段,最終拼接成一段攻擊代碼

    比如輸入用戶名和密碼的時候不是輸入的用戶名和密碼而是 sql 片段,這樣的話後端拿到用戶提交的用戶名和密碼之後和數據庫通信進行驗證的時候,有可能就是一段攻擊代碼。

  • 預防措施:使用 mysqlescape 函數處理輸入內容即可

下面以自己開發的博客項目中登錄時的代碼為例進行說明 :

博客項目中登錄時的關鍵代碼:

const sql = `
select username, realname from user 
where username ='${username}' and password='${password}';
`;

項目代碼中登錄的實現:利用用戶傳遞過來的 usernamepasswordMySQL 數據庫中查詢,如果 usernamepassword 在數據庫中存在,那麼返回 username,並將其存入 redis 中,通過 req.session.username 是否存在來判斷用戶是否登錄,如果登錄不成功 req.session.usernameundefined

上面的代碼是漏洞百出,如果一個用戶這樣寫用戶名和密碼,想一下能不能登錄成功?

username = zhangsan' --  password = 111

MySQL 中對應的代碼:

select username, realname from user where username ='zhangsan' -- ' and password='111';

可以看到 -- 後面的語句都被注釋掉了,等價於下面語句:

select username, realname from user where username ='zhangsan'

不用多想,登錄肯定成功呀,只要能夠知道用戶名就可以登錄上去。

如果黑客想刪除你的數據庫也很容易:

select username, realname from user where username ='zhangsan'; delete from user -- ' and password='111';

這樣在執行完查詢用戶名為 zhangsan 的信息之後,緊接着會刪除 user 表中的所有內容。

sql 注入防禦措施

利用 MySQL 提供的 escape 函數將所有出現在 sql 語句中的變量都執行一遍,以上面的代碼為例。

注意 const sql = “ 中 ${username} 在使用 escape 函數之後已經不需要再加引號了。

username = escape(username);
password = escape(password);
// 使用 escape 函數運行完之後的函數就不需要在 sql 中加引號了
const sql = `
select username, realname from user 
where username =${username} and password=${password};
`;

現在 username 還是 zhangsan' --password 還是 111

利用 escape 函數之後生成的 sql 語句:

sql = select username, realname from user 
where username ='zhangsan\' -- ' and password='111';

這時候 username 中間的 '\ 轉義了, -- 也不能起注釋作用了。

不只是登錄這個地方只要是由用戶輸入的而且要在 sq 語句中出現的變量,都要在 escape 函數中執行一遍,注意在 sql 語句中 ${變量} 兩邊就不需要加引號了。

Tags: