說起前端安全問題,大部分都聽過 XSS 和 CSRF 這兩個名詞,前端面試中我們也經常會問這兩個點作為 web 安全的一些基礎考察。但大部分只是從淺談輒止,停留在基本詞義,很少有人真正去主動實踐過安全,並且思考背後的關聯。
前端安全主要來源兩個方面,一個是不安全的指令碼、另一個是不安全的請求,前者代表各種 XSS 等注入指令碼的手段、後者則發起非法的請求,CSRF 是代表。
web 安全基石——同源策略
這篇文章主要想分析下以跨域為代表的安全攻擊和保護。很多人知道同源策略SOP,它是 web 的安全基石。由於瀏覽器是 http 無狀態的連線,所以對於不同源頭的區別至關重要,簡而言之:協議、域名、埠三者相同,瀏覽器則認定它為同源,在同一源下,諸如、cookie\html5儲存等資源是可以共享的。
有了同源策略作為保證,為什麼會出現其他源的非法請求呢?
這裡就是很多人容易混淆的地方。同源策略的核心是保護屬於某個源的資源不被其他源讀取,它們彼此獨立,這個設計的初衷並非為防止非法請求而存在,也就是說同源不等於合法請求,它也不關心請求是否合法。簡單的定義為“可寫不可讀”。
那麼問題來了,為什麼我們在跨域請求的時候會被瀏覽器會有報錯呢?(後面有解釋)
上圖是請求是被 CORS 規則阻止的錯誤提示。
跨域共享策略
同源策略是保護統一源資源部被竊取,但現實是,我們很多場景,需要在不同源之間共享,跨源資源共享(CORS)既是針對如何合法的突破 SOP。在形成官方協議前,曾經有各自非官方的手段來突破不同源之間共享資源,比如 JSOP 。CORS 是一種官方的協議用來在不同源直接共享資源,所以它目的也不是為了安全,某種程度,正因為不合理的使用,反而不安全,比如使用這樣的協議: Access-Control-Allow-Origin: *允許所有源共享資源,無疑是向入侵者敞開大門。
所以、SOP 和 CORS 是資源隔離和共享的相反方向。
CSRF 的本質
之所以強調這一點,是要告訴開發者,瀏覽器所做的只不過是實現相關基礎協議,不要試圖誤解這兩個協議會保護你的應用,應用的安全應該由你自己掌控。
回到 CSRF 的場景,攻擊者在另一個非法的源對你的源發起請求,騙過伺服器。為什麼伺服器會相信一個非法的源呢,或者說伺服器如何保證這個源是合法的。早在一份統計中,至少百分之的網站 30% 有 CSRF 漏洞,很多開發者對 CSRF 認識不夠深刻。由於 http 是無狀態的,前後端會話的維持和使用者身份校驗大部分是基於 cookie。cookie 的資料預設會受到 SOP 策略的保護——既不同源之間是無法共享的。但是,瀏覽器並不保證每次請求中,cookie 是否在同一源,這就是 CSRF 中招的根源。
也就是說,在不同的源下請求相同的介面,cookie 會攜帶過去,如果只是依賴 cookie 中的 token 登入態去校驗使用者身份, 那麼完全是不夠安全的。假設使用者在 A 站點登入了個人賬戶,這個站點基於cookie 維護的會話,同時沒有做任何 scrf 保護,接下來,它收到一份中間郵件,點選開啟一個釣魚網頁,這個網頁就能像 A 站點傳送任何攜帶登入態的資料,任意修改這個使用者 在 A 站點的資料。
相比不少同學有疑惑,我們剛剛在上面不是展示了,CORS 協議的存在會讓控制檯報錯,提示請求失敗嗎?就算是釣魚網頁請求 A 站點可以攜帶使用者登入後的 cookie ,這個請求不是沒有發出去嗎
這個錯誤容易被誤解,跨域資源共享的報錯代表的不是請求失敗,而是資源讀區失敗。也就是說,瀏覽器雖然在介面跨域的邏輯中報錯,但 http 請求過錯仍然是成功的,它抵達了後臺,同時返回了對應的結果,但瀏覽器基於 SOP 策略讓你無法訪問這個資料,才有了這個提示。
這裡再一次證明了,SOP 只服務於源的資料隔離,它對請求不做任何限制,甚至跨域的時候 cookie 攜帶也不做限制。大量 CSRF 的漏洞根源也在於此。
再重複一遍,基於 cookie 維護的登入態,如果沒有做任何其他手段的校驗,後臺收到這個攜帶登入 cookie 的 http 請求,不一定是“合法”的。最簡單的測試我們的介面是否有 CSRF 保護的方法是,把請求內容複製下來,在控制檯做一次 http 重放,如果成功返回,那麼基本代表是有漏洞的。
CSRF 的保護
跨域請求攻擊的本質是利用 cookie 會攜帶,最簡單的保護手段就是不使用 cookie,改成 html 儲存,比如 localstorage 等,然後前後自行維護一套類似 cookie 的過期邏輯。
但是,cookie 雖然有很多問題,但它的初衷就是給無狀態 http 請求提供一個身份記錄,也不能完全因噎廢食,在做好安全的前提下,它仍然是最簡單的方案,下面是一些常見的保護措施。
校驗源
瀏覽器每次 http 請求會攜帶兩 header 欄位,Referer 和 Origin 分別表述請求的 url 和請求的源,Referer 因為會包含全部的路徑,正在成為被廢棄的欄位,而 Origin 只有協議、域名和埠,正好符合 SOP 源定義的規則,我們用 Origin 判斷是最合適的。
源的校驗只能作為輔佐手段,而不是最終判定,因為 Origin 欄位在各大瀏覽器中曾經不斷暴露過很多漏洞,所以不能嚴格依賴。
CSRF Token
這是教科書式的方案,但仍然想重點講下,因為最近我研究了國內大廠的某 web 站點,竟然發現有嚴重的 scrf 的漏洞。
上面說過,CSRF 漏洞的根源在於信任 cookie,token 就是我們給每個使用者分配的一個隨機的牌,每次請求通過校驗這個令牌判斷來源是否正確。這個 token 本質上替代了校驗源 origin 的作用,只不過由我們自己生成和控制。
有人會問?攻擊者能不能拿到/利用這個 token 呢,答案是,由於 SOP策略的存在,攻擊者不能拿到第三方域的任何資料,包括 cookie 和其他內容。
但是,如果你的 token 沒有正確的儲存,仍然可以被利用。scrf 的精髓在利用 cookie 自定攜帶的特點,所以CSRF token 是不能儲存在 cookie,除此以外,其他任何地方都行,比如 dom、localstorage 等。
隨機 cookie 驗證
SOP 不會阻止 cookie 跨域傳送,但是會阻止 cookie 獲取。我們可以利用這點特性,為每次請求返還一個隨機的值設定在 cookie 中,下一次請求的時候,把這個值用 js 從 cookie 中取出來,拼接到引數中,後臺在收到介面時,先校驗這個引數。即可判定是不是可信任的源。 由於在第三方源無法用 js 取出這個隨機值,那麼由它發起的請求自然也會被過濾掉。
Samesite Cookie
CSRF 的根源在 cookie 可以被跨域傳送,Samesite Cookie (opens new window)是 Google起草的一份草案來改進HTTP協議,簡而言之,可以給 cookie 宣告是否僅限於第一方或者同一站點上下文。由於是新協議,在這用這個特性的同時,需要注意下瀏覽器相容的版本。
繞過 SOP
雖然可以用 scrf token 等方式保護 CSRF 攻擊,但跨域攻擊的方式往往嘗試各種手段非法繞過SOP,其最脆弱的部分還是使用者和開發人員本身。下面舉一些例子。
iframe 介面偽裝
CSRF token 可以保護使用者介面不被偽造,但是如果將一個攻擊目標的 url 放在被控制的域名的 iframe 中,這個 iframe 預設會載入目標 url 內的 token。攻擊者可以對 iframe 進行透明化處理,並且將它層級提升到位餘主介面的誘導按鈕上。當誘導按鈕的位置剛好與 iframe 內某個請求的按鈕重合,那麼這次點選就在使用者不知情的背景下進行了“跨域請求”。這種攻擊也叫點選劫持 (opens new window)。
應對的方式是:可以通過 X-Frame-Options 引數定義允許巢狀的源。
SOP 與 CORS
SOP 是對不同源資源的限制,而 CORS 是一種開放限制的官方策略,不合理的使用會帶來風險,比如:
Access-Control-Allow-Origin: *
設定許可權過大等同於沒有設定。
源的更改
window.domain 可以對當前源進行更改,在一些子域和父域通訊的場景下可能需要用到。子域一旦通過 window.domain 更改為父域,意味著更大的範圍,那麼它將會在任何子域下有跨域訪問許可權。
// 對當前域 store.abc.com 進行更改
document.domain = abc.com
// hack.abc.com 可以對 store.abc.com 進行跨域訪問
CSRF 攻擊示例(僅僅用於測試)
對於安全問題,往往是需要知己知彼,才能有所準備。下面是一些 CSRF 攻擊中的示例。
<iframe >
// 目標網站
<form method="post" action="https://demodexx.com/modify-password">
// 通過介面分析得到對方的欄位名
// 設定攻擊者想要的新密碼
<input name="password" value="123456">
<input type="button" value="點選領取 100萬大獎">
</form>
</iframe>
當使用者進入一個攻擊者控制的源,在隱藏的表單中誘導點選提交表單,這個時候,會自動攜帶 demodexx 的 cookie 進行提交,如果對方處於登入態,密碼就直接被修改了。
通過樣式偽裝的表單是常用的手段,它比 ajax 請求有幾個優勢,第、相容性問題小,第二、避開跨域預檢請求 (opens new window)。
下面是 ajax 模擬示例,Content-Type 的值限於三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded 就不會有預檢請求
const x = new XMLHttpRequest();
x.open('POST', 'url')
x.withCredentials = true // 允許攜帶 cookie
x.responseType='application/x-www-form-urlencoded';
const d = new FormData()
d.append('password', '12345')
x.send(d)
結
很多跨域安全問題是由於瀏覽器 “預設許可” 這一不成文的規定帶來的,之所以這樣做,也許是基於開放互通的 web 思維,或者瀏覽器廠商為了佔據更多市場而提供更開放自由的行為。
無論如何,對於開發者,前端資源的讀、寫許可權,不該信任其他第三方源,後臺服務預設不信任任何請求(包括自己的域)。用一種預設拒絕的心態處理一切安全問題。
——————
文件資訊
發表時間:2022-01-24
筆名:混沌福王
版權宣告:如需轉載,請郵件知會[imwangfu@gmail.com],並保留此申明
——————