2020年最全前端業務安全綜述

程洋cYang發表於2020-12-09

前言

隨著前端的發展,前端應用正在迅速變化。 前端程式碼承擔著與後端程式碼幾乎相同的責任,可以做更多的事情,隨著公司體系越來越完善,開發框架和平臺的不斷成熟,需要開發者考慮的安全問題越來越少,但並不是開發者就不需要關心專案的安全問題。

本文主要介紹幾種業務開發中經常遇到的幾種前端安全問題,由於篇幅有限本文點到為止,後續有機會會逐一展開來講,本文提供大量的圖例來說明問題。

1. XSS

XSS是跨站指令碼攻擊的簡寫,攻擊者想盡一切方法 將一段指令碼內容放到目標網站的目標瀏覽器上解釋執行。攻擊者將惡意指令碼輸入到目標網站中。 當其他使用者訪問該網站的時候,由於瀏覽器不知道它是由網站提供服務的指令碼還是攻擊者埋入的指令碼,因此將執行此該指令碼。攻擊者就可以很容易利用埋入的指令碼進行攻擊。

  1. 攻擊者編寫惡意攻擊的指令碼
  2. 攻擊者訪問前端頁面,在輸入框中輸入編寫好的惡意指令碼
  3. 攻擊者將惡意指令碼進行提交,後端將惡意指令碼儲存在資料庫中
  4. 當某些合法使用者訪問該網站的時候,該網站會獲取儲存在資料庫中的惡意指令碼,但是瀏覽器不知道它是惡意指令碼所以執行了。

其實就相當於攻擊者在使用者端的頁面上注入了一段指令碼,有了這段指令碼攻擊者就可以為所欲為了

防範
  1. 永遠不要相信使用者的輸入,對使用者輸入的特殊字串進行轉譯,針對使用者的輸入設定標籤白名單
  2. cookie設定HttpOnly,配合token或驗證碼防範
  3. 設定CSP安全策略-可以通過兩種方式設定CSP,一種是meta標籤,一種是HTTP響應頭Content-Security-Policy

2. CSRF

CSRF是跨站請求偽造的簡寫,一種誘騙受害者提交惡意請求的攻擊,攻擊者盜用了你的身份,以你的名義傳送惡意請求,請求到達後端時,伺服器將無法區分惡意請求和合法請求。。CSRF能夠做的事情包括:以你名義傳送郵件,發訊息,盜取你的賬號,甚至於購買商品,虛擬貨幣轉賬等。

CSRF攻擊必須具備兩個流程

  1. 登入受信任網站A,並在本地生成Cookie。
  2. 在不登出A的情況下,訪問危險網站B。
防範
  1. 同源檢測,直接禁止外域(受信域可以開白名單)對我們發起請求
  2. CSRF Token,就把Token以引數的形式加入請求了,提交給伺服器的時候,伺服器需要判斷Token的有效性
  3. Samesite Cookie屬性,Samesite=Strict只允許同源網站提交請求攜帶cookie

3. 網路傳輸安全

中間人 (Man-in-the-middle attack, MITM) 是指攻擊者與通訊的兩端分別建立獨立的聯絡, 並交換其所收到的資料, 使通訊的兩端認為他們正在通過一個私密的連線與對方直接對話, 但事實上整個會話都被攻擊者完全控制. 在中間人攻擊中, 攻擊者可以攔截通訊雙方的通話並插入新的內容。

是不是覺得有了https網路傳輸安全問題就迎刃而解了呢,即使被中間人攔截了,資料也是加密的。其實不是這樣的,不知道大家有沒有使用過charles進行抓包呢,如果資料都是加密的,為啥charles抓包後我們能夠看到傳輸的明文呢,其實這就是中間人攻擊。

charles中間人劫持

防範
  1. 對於個人來說防止自己被中間人攻擊最基本的就是不要亂連不信任的網路
  2. 公司APP來說應該配置禁止被抓包
  3. APP和瀏覽器都應該嚴格校驗證照,不使用不安全的APP和瀏覽器

4. 介面加簽

通過上面的例子我們知道https並不是絕對安全的,他是會被中間人劫持的,那麼我們有什麼方法防止資料被串改呢?

介面加簽的目的是防止資料被串改!

舉兩個例子

例子1:正常使用者提交轉賬申請,請求中攜帶正常使用者的使用者資訊,他想轉賬N金額給使用者A,這樣的請求銀行沒法拒絕會正常轉賬,因為攜帶了正常的使用者資訊。但是當中間人劫持了這個請求,他修改了轉賬賬號為B,修改了轉賬金額為M,這樣我們的錢會不會轉給其他人呢?

例子2:我們辛辛苦苦寫了一個運營小遊戲,違規使用者隨便玩了一下得分為0,但是他通過Charles攔截了這個請求,修改了得分為10000,然後進行提交,我們的正常伺服器能否知道分數是被串改的呢?

為了解決上述問題,我們可以引入介面加簽

服務端閘道器首先會校驗籤是不是對的,如果不對直接拒絕請求,而籤的生成和請求引數密密相關,當介面請求中的引數被串改後,閘道器是沒法進行驗籤通過的,直接拒絕了請求,丟擲錯誤。

5. 介面加密

有時候我們的引數根本不想被人看見是啥,我們就可以利用引數加密了

介面防重放

防重放也叫防複用,簡單來說,就是我獲取到這個請求的資訊之後, 我什麼也不改, 我就拿著介面的引數去重複請求這個充值的介面,也就是說我的請求是合法的,因為所有引數都是跟合法請求一模一樣的,也就是說: 服務端的 sign 驗證一定能通過。如圖上的例子,即使我們不知道登入賬戶名密碼,即使介面引數被加簽加密了,我們依舊能夠登入並拿到登入資訊,我們根本不用關心加密加簽的邏輯,我們只需要簡單的重放攻擊即可。

防重放設計
  1. 客戶端在請求中新增兩個引數
    1.1 新增一個隨機不重複的字串引數 比如uuid 至於怎麼讓他不重複,可以考慮拼接時間戳,md5隨機數等
    1.2 新增一個請求時間的引數 如 time 值就是傳送請求時的 時間戳
  2. 服務端接收到請求之後:
    2.1 去快取裡中查詢 uuid 這個引數對應的值是否存在
    2.2 如果不存在: 就把這個uuid的值儲存到快取中, 記錄這個請求
    2.3 如果已存在: 存在那就證明, 已經請求過一次了, 就不處理這個請求了

這就是最簡單的防重放邏輯,介面只能呼叫一次,即使被中間人攻擊後也沒法進行重放

6. 環境檢測

是不是瀏覽器

是不是我們檢測上述變數就認為是瀏覽器環境呢,其實不是這樣的,上面的變數都是可以被串改的,所以可以作為參考,絕對不能過分的依賴!下面列舉幾項處理方案,可以看到當我們檢測這些變數的時候,這些變數都是可以被串改的。

檢測變數 對抗處理方案
navigator.languages Object.defineProperty(navigator, 'languages', { get: () => ["zh-CN", "zh", "en"] });
navigator.plugins Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
是不是模擬器

一般我們檢測到這些變數的時候可以無腦的認為就是模擬器,比如Puppeteer中我們啟動的時候,navigator.webdriver這一屬性的值等於true的,常規瀏覽器中由於沒有這個屬性navigator.webdriver的值等於undefined的。

Object.defineProperty(navigator, 'webdriver', {
	get: () => undefined,
});

攻擊者這樣串改後我們是不是就沒有辦法知道是不是webdriver了呢?其實我們還是有辦法判斷的,因為這邊只是返回了navigator.webdriver的值是非的,但是navigator上依舊有webdriver這個屬性,我們有沒有辦法檢測屬性是否存在呢?其實我們很容易拿到navigator上所有屬性的。

var attr = window.navigator, result = [];
do {
    Object.getOwnPropertyNames(attr).forEach(function(a) {
        result.push(a)
    })
} while (attr=Object.getPrototypeOf(attr));

當我們判斷navigator上有webdriver這個屬性的時候,就可以簡單的認為這個是模擬器環境,是不是覺得很完美的判斷了是不是模擬器了,其實不是的,攻擊者甚至可以刪除掉webdriver屬性。

delete navigator.__proto__.webdriver

這樣之後就完全抹去webdriver變數了,通過這個辦法來判斷是不是模擬器就沒有路子了。

有沒有使用者行為

通常我們可以通過判斷事件上的isTrusted屬性來判斷是不是真實的事件,大部分情況我們都能夠很好的處理,但是攻擊者是很可怕的,這些簡單的伎倆他們能夠輕輕鬆鬆的繞過,他可以重寫事件啊,比如:

function clone(e) {
    const t = {};
    for (let attr in e) {
        if (typeof e[attr] === "function") {
            t[attr] = e[attr];
        } else {
            Object.defineProperty(t, attr, {
                get: () => {
                    if (attr === 'isTrusted') {
                        return true;
                    }
                    return e[attr];
                },
                set: v => {
                    e[attr] = v;
                }
            });
        }
    }
    return t;
}
const oldAEL = document.addEventListener;
window.addEventListener = document.addEventListener = function (e, func, c) {
    const newFunc = function (event) {
        const newEvent = clone(event);
        return func(newEvent);
    };
    return oldAEL.call(this, e, newFunc, c);
};

通過上面的例子我們發現,不管我們怎麼攻防,攻擊者都是有辦法繞過去的。其實上面還都是簡單的攻防,攻擊者甚至可以自己定製瀏覽器,當我們的頁面跑在攻擊者定製的瀏覽器中的時候,通過上面的那些方法我們真的無能為力了,那麼是不是我們只能放棄了呢,其實不是的。

辨別機器行為還是得需要驗證碼

7. 無處不在的驗證碼

驗證碼這個名詞真正被發明出來是在2003年,這比很多概念晚多了,比如神經網路70年代就已經有很多人在研究。卡內基梅隆大學的Luis von Ahn,Manuel Blum, Nicholas J.Hopper等人首次提出了“CAPTCHA”這個詞。他們對驗證碼系統做的很深刻的研究,並且將其付諸程式化。自此大量的驗證碼開始被應用到網站中,有效的阻止了黃牛軟體的肆虐。時至今日,每天有過億的驗證碼被人們不斷地輸入著。

傳統驗證碼

傳統驗證碼易被各影像識別軟體、打碼平臺輕易破解,人工智慧飛速發展,因此扭曲的文字驗證方式也不再是一個可靠的方法,據說已經能夠解決99.8%的圖片字元型驗證碼。由此誕生了很多新型的驗證碼型別,其中國內最具代表的就是極驗,國外的就是谷歌的reCAPTCHA,他們帶來了一種全新的模式。

新型驗證碼

新型驗證碼不僅很難破解,他的互動會更加的友善,甚至做到無驗證碼,只有在需要進行驗證的時候才出來。下面是網易易盾的產品流程圖,其他產品都基本類似。背後依託強大的機器學習判斷行為到底是不是人。

8. 程式碼加密混淆

程式碼加密混淆大大降低了前端程式碼的可讀性,同時一定程度上會增加程式碼的體積。但是對於非常核心的業務邏輯,程式碼加密是非常有必要的,比如:

  1. 前端加簽程式碼,由於加簽是在前端進行的,前端必須存有祕鑰和加簽規則,但是一旦被第三方知道加簽的祕鑰和規則,加簽也就不公而破了,所以加簽的前端程式碼必須得加密。
  2. 新型驗證碼使用者行為採集程式碼,新型驗證碼涉及很多使用者行為的前端採集,然後提交後端分析,如果採集規則被第三方知道,那麼攻擊者也就很好的進行攻擊行為,所以採集程式碼也是需要加密的。

歡迎關注公眾號:前端複習課,一起分享交流前端知識

相關文章