瀏覽器中存儲訪問令牌的最佳實踐
瀏覽器提供了各種持久化數據的解決方案。當存儲令牌時,您應該權衡存儲選擇與安全風險。
譯自Best Practices for Storing Access Tokens in the Browser。
web應用程序不是靜態站點,而是靜態內容和動態內容的精心組合。更常見的是,web應用程序邏輯在瀏覽器中運行。與從服務器獲取所有內容不同,應用程序在瀏覽器中運行JavaScript,從后端API獲取數據,并相應地更新web應用程序呈現。
為了保護數據訪問,組織應該采用OAuth 2.0。通過OAuth 2.0,JavaScript應用程序需要在對API的每個請求中添加訪問令牌。出于可用性原因,JavaScript應用程序通常不會按需請求訪問令牌,而是存儲它。問題是,如何在JavaScript中獲取這樣的訪問令牌?當您獲取一個令牌時,應用程序應該在哪里存儲令牌,以便在需要時將其添加到請求中?
本文討論了瀏覽器中可用的各種存儲解決方案,并突出了與每種選擇相關的安全風險。在審查威脅之后,它描述了一種解決方案,以提供最佳的瀏覽器安全選項,用于必須與OAuth保護的API集成的JavaScript應用程序。
獲取訪問令牌
在應用程序可以存儲訪問令牌之前,它需要先獲取一個令牌。當前的最佳實踐建議通過“授權碼流”這一方式來獲取訪問令牌: 授權碼流是一個兩步流程,首先從用戶那里收集一個授權許可——授權碼,然后應用程序在后臺通道中用授權碼交換訪問令牌。這個請求稱為令牌請求,例子如下:
const accessToken = await fetch(OAuthServerTokenEndpoint, {
method: "POST",
// token request with authorization code and PKCE
// submits data in as x-www-form-urlencoded encoded format
body: new URLSearchParams({
client_id: "example-client",
grant_type: "authorization_code",
code: authorization_code,
code_verifier: pkce_code_verifier
})
})
// server responds with JSON object
.then (response => response.json())
.then (tokenResponse => {
// parse access token from response
if (tokenResponse.accessToken) {
return tokenResponse.accessToken;
} // else handle error response
})
.catch(
// handle network error
)
請注意,任何人都可以檢查瀏覽器加載的資源,包括任何JavaScript代碼。因此,任何用JavaScript實現的OAuth客戶端都被認為是一個公開客戶端——一個無法保密的客戶端,因此在令牌請求期間無法進行身份驗證。然而,代碼交換的證明密鑰(Proof Key for Code Exchange,PKCE)提供了一種方法來確保公開客戶端的授權碼流的安全性。為了減輕與授權碼相關的風險,在使用授權碼流時,始終應用PKCE。
瀏覽器威脅
跨站請求偽造(CSRF)
在跨站請求偽造(CSRF)攻擊中,惡意行為者會欺騙用戶通過瀏覽器無意中執行惡意請求。例如,攻擊者可以在網站中嵌入精心設計的圖像源字符串,以觸發瀏覽器運行GET請求,或者在惡意網站上添加表單,以觸發POST請求。在任何情況下,瀏覽器都可能會自動將cookie(包括單點登錄cookie)添加到這樣的請求中。
CSRF攻擊也被稱為“會話騎乘”,因為攻擊者通常會利用用戶的經過身份驗證的會話來進行惡意請求。因此,攻擊者可以默默地代表用戶執行請求,并調用用戶可以調用的任何端點。然而,攻擊者無法讀取響應,所以他們通常以一次性狀態更改請求為目標,如更新用戶的密碼。
跨站腳本(XSS)
跨站腳本(XSS)漏洞允許攻擊者將惡意的客戶端代碼注入到一個本來受信任的網站中。例如,如果用戶輸入生成的輸出沒有被適當清理,web應用程序的任何地方都可能存在漏洞。瀏覽器會自動在受信任的網站的上下文中運行惡意代碼。
XSS攻擊可用于竊取訪問令牌和刷新令牌,或執行CSRF攻擊。不過,XSS攻擊有一個時間窗口,因為它們只能在有限的時間段內運行,如令牌的有效期內,或者打開的選項卡存在漏洞的時長。
即使在XSS無法用于檢索訪問令牌的情況下,攻擊者也可以利用XSS漏洞通過會話騎乘向有保護的Web端點發送經過身份驗證的請求。然后,攻擊者可以偽裝成用戶,調用用戶可以調用的任何后端端點,并造成嚴重損害。
瀏覽器中的存儲解決方案
應用程序收到訪問令牌后,需要存儲該令牌以在API請求中使用它。瀏覽器中有多種方法可以持久化數據。應用程序可以使用專用API(如Web存儲API或IndexedDB)來存儲令牌。應用程序也可以簡單地將令牌保存在內存中或將其放在cookie中。一些存儲機制是持久的,另一些在一段時間后或頁面關閉或刷新后會被清除。
一些解決方案跨選項卡共享數據,而其他解決方案僅限于當前選項卡。但是,本指南中介紹的大多數方法都針對每個源存儲數據。因此,對于任何相關討論來說,理解一些概念很有幫助:origin和site。
某個(Web)資源的origin是其URL的scheme、hostname和port。例如,https://example.com/number/one和https://example.com:80/path/two具有相同的origin,因為它們共享scheme(https)、hostname(example.com)和端口(默認端口)。它們的origin為https://example.com,與https://example.com:8443或https://this.example.com不同,因為它們在端口和主機名上有所不同。
相比之下,一個site比資源的origin要大。一個站點是為一組資源提供服務的Web應用程序的通用名稱。簡單地說,一個站點是scheme和domain name,如https://example.com。雖然https://example.com和https://this.example.com:8443有不同的origin(不同的主機名和端口),但它們是相同的站點,因為它們托管在同一個域名(example.com)上并使用相同的scheme(https)。(從技術上講,這個定義還有細微差別,但這個簡化的說法有助于解釋這個概念)。
本地存儲
本地存儲是通過Web存儲API中的全局localStorage對象以JavaScript訪問的。本地存儲中的數據在瀏覽器選項卡和會話之間可用,也就是說它不會過期或在瀏覽器關閉時被刪除。因此,通過localStorage存儲的數據可以在應用程序的所有選項卡中訪問。因此,在本地存儲中存儲令牌非常誘人。
// Storing the access token
localStorage.setItem("token", accessToken);
// Loading the access token
let accessToken = localStorage.getItem("token");
每當應用程序調用API時,它都會從存儲中獲取令牌并手動添加到請求中。但是,由于本地存儲可以通過JavaScript訪問,這意味著該解決方案也容易受到跨站腳本(XSS)攻擊。
如果您在本地存儲中使用access token,并且攻擊者設法在您的應用程序中運行外部JavaScript代碼,那么攻擊者可以竊取任何令牌并直接調用API。此外,XSS還允許攻擊者操作應用程序中的本地存儲數據,這意味著攻擊者可以更改令牌。
請注意,本地存儲中的數據會永久存儲,這意味著存儲在其中的任何令牌會駐留在用戶的設備(筆記本電腦、電腦、手機或其他設備)的文件系統上,即使瀏覽器關閉后也可以被其他應用程序訪問。因此,在使用localStorage時,請考慮終端安全性。考慮并防止瀏覽器之外的攻擊向量,如惡意軟件、被盜設備或磁盤。
根據上述討論,請遵循以下建議:
- 不要在本地存儲中存儲敏感數據,如令牌。
- 不要信任本地存儲中的數據(尤其是用于認證和授權的數據)。
會話存儲
會話存儲是Web存儲API提供的另一種存儲機制。與本地存儲不同,使用sessionStorage對象存儲的數據在選項卡或瀏覽器關閉時會被清除。此外,session存儲中的數據在其他選項卡中不可訪問。只有當前選項卡和origin中的JavaScript代碼可以使用相同的會話存儲進行讀取和寫入。
// Storing the access token
sessionStorage.setItem("token", accessToken);
// Loading the access token
let accessToken = sessionStorage.getItem("token");
與本地存儲相比,會話存儲可以被認為更安全,因為瀏覽器會在窗口關閉時自動刪除任何令牌。此外,由于會話存儲不在選項卡之間共享,攻擊者無法從另一個選項卡(或窗口)讀取令牌,這減少了XSS攻擊的影響。
在實踐中,使用sessionStorage存儲令牌的主要安全問題是XSS。如果您的應用程序容易受到XSS攻擊,攻擊者可以從存儲中提取令牌并在API調用中重放它。因此,會話存儲不適合存儲敏感數據,如令牌。
IndexedDB
IndexedDB是索引數據庫API的縮寫。它是一個用于在瀏覽器中異步存儲大量數據的API。但是,在存儲令牌時,這個瀏覽器API提供的功能和容量通常不是必需的。由于應用程序在每次API調用中都發送令牌,最好是使令牌的大小最小化。
與迄今為止討論的其他客戶端存儲機制一樣,使用索引數據庫API存儲的數據訪問受到同源策略的限制。只有相同來源的資源和服務工作者才能訪問數據。從安全角度來看,IndexedDB與本地存儲相當:
- 令牌可能會通過文件系統泄露。
- 令牌可能會通過XSS攻擊泄露。
因此,不要在IndexedDB中存儲訪問令牌或其他敏感數據。IndexedDB更適合用于應用程序脫機工作所需的數據,如圖像。
內存
存儲令牌的一個相當安全的方法是將其保存在內存中。與其他方法相比,令牌不存儲在文件系統中,從而減輕了與設備文件系統相關的風險。
最佳實踐建議在內存中存儲令牌時將其保存在閉包中。例如,您可以定義一個單獨的方法來使用令牌調用API。它不會向主應用程序(主線程)透露令牌。下面的摘錄顯示了如何在JavaScript中使用內存處理令牌的示例。
function protectedCalls(tokenResponse) {
const accessToken = tokenResponse.accessToken;
return {
// call API with access token
getOrders: () => {
const req = new Request("https://server.example/orders");
req.headers.set("Authorization", accessToken);
return fetch(req)
}
}
}
const apiClient = protectedCalls(tokenResponse);
// call protected API
apiClient.getOrders();
請注意,攻擊者可能無法在獲取令牌后直接訪問令牌,因此可能無法直接使用令牌調用API。即便如此,通過持有令牌引用的apiClient,他們可以隨時通過apiClient調用API。但是,任何此類攻擊都限于選項卡打開并且接口提供的功能的時段。
除了與潛在的XSS漏洞相關的安全問題外,在內存中保持令牌的最大缺點是頁面重載時令牌會丟失。然后,應用程序必須獲取一個新令牌,這可能會觸發新的用戶身份驗證。安全的設計應考慮到用戶體驗。
使用服務工作者的體系結構通過在獨立的線程中運行令牌處理功能來減輕可用性問題,該線程與主網頁分離。服務工作者實際上充當應用程序、瀏覽器和網絡之間的代理。因此,它們可以攔截請求和響應,例如緩存數據和啟用離線訪問,或者獲取和添加令牌。
在使用JavaScript閉包或服務工作者處理令牌和API請求時,XSS攻擊可能會針對OAuth流程,如回調流或靜默流來獲取令牌。它們可以取消注冊并繞過任何服務工作者,或者使用原型污染“實時讀取令牌”通過覆蓋諸如window.fetch之類的方法。因此,請出于方便而不是安全性考慮JavaScript閉包和服務工作者。
Cookie
Cookie是存儲在瀏覽器中的數據片段。由設計,瀏覽器會將cookie添加到對服務器的每個請求中。因此,應用程序必須謹慎使用cookie。如果未經仔細配置,瀏覽器可能會在跨站請求時追加cookie,并允許跨站請求偽造(CSRF)攻擊。
Cookie具有控制其安全屬性的屬性。例如,SameSite屬性可以幫助緩解CSRF攻擊的風險。當一個cookie的SameSite屬性設置為Strict時,瀏覽器只會將其添加到源自并目標與cookie的源站點相同的請求中。當請求嵌入在任何第三方網站中時,瀏覽器不會添加cookie,例如通過鏈接。
您可以通過JavaScript設置和檢索cookie。但是,當使用JavaScript讀取cookie時,應用程序會變得容易受到XSS攻擊(除了CSRF之外)。因此,首選的選擇是讓后端組件設置cookie并將其標記為HttpOnly。該標志可以緩解通過XSS攻擊泄露數據的問題,因為它指示瀏覽器cookie不能通過JavaScript訪問。
為防止cookie通過中間人攻擊泄露,這可能導致會話劫持,cookie應僅通過加密連接(HTTPS)發送。要指示瀏覽器僅在HTTPS請求中發送cookie,必須將Secure屬性設置為cookie。
Set-Cookie:token=myvalue;SameSite=Strict;Secure;HttpOnly
與瀏覽器中的任何其他永久存儲解決方案一樣,cookie可能會駐留在文件系統中,即使瀏覽器已關閉(例如,cookie不必過期,或者瀏覽器可以將會話cookie作為恢復會話功能的一部分保留)。為了減輕從文件系統中竊取令牌的風險,只能在cookie中存儲加密的令牌。因此,后端組件只能在Set-Cookie頭中返回加密的令牌。
威脅矩陣
下表總結了瀏覽器中存儲解決方案的威脅評估,主要威脅向量標記為紅色。橙色威脅需要除Web技術之外的緩解措施。綠色威脅已經或可以通過適當的設置成功消除。
圖片
無論攻擊者何時設法竊取令牌,只要令牌有效,他們就可以獨立于用戶和應用程序使用訪問令牌。如果攻擊者設法竊取刷新令牌,他們可以顯著延長攻擊時間并增加損害,因為他們可以續新訪問令牌。黑客甚至可以將攻擊擴展到除JavaScript應用程序使用的API之外的其他API。例如,攻擊者可以嘗試重放訪問令牌并利用不同API中的漏洞。
被盜的訪問令牌可能會造成嚴重損害,XSS仍然是Web應用程序的主要問題。因此,避免在客戶端代碼可以訪問的地方存儲訪問令牌。相反,將訪問令牌存儲在cookie中。當使用適當的屬性配置cookie時,瀏覽器泄露訪問令牌的風險為零。然后,XSS攻擊與在同一站點上的會話劫持攻擊相當。
使用Cookie的OAuth語義
Cookie仍然是傳輸令牌和充當API憑據的最佳選擇,因為即使攻擊者成功利用XSS漏洞,也無法從cookie中檢索訪問令牌。但是,為了做到這一點,cookie必須適當配置。
首先,將cookie標記為HttpOnly,以便它們不可通過JavaScript訪問,以解決XSS攻擊的風險。另一個關鍵屬性是Secure標志,它確保cookie僅通過HTTPS發送,以減輕中間人攻擊。
其次,頒發短暫的只在幾分鐘內有效的訪問令牌。在最壞的情況下,具有最小有效期的訪問令牌只能在可以接受的短時間內被濫用。通常認為15分鐘的有效期是合適的。讓cookie和令牌的過期時間大致相同。
第三,將令牌視為敏感數據。只在cookie中存儲加密令牌。如果攻擊者設法獲取加密令牌,他們將無法從中解析任何數據。攻擊者也無法將加密的令牌重放到任何其他API,因為其他API無法解密令牌。加密令牌只是限制了被盜令牌的影響。
第四,在發送API憑據時要限制性強。只向需要API憑據的資源發送cookie。這意味著確保瀏覽器只在實際需要訪問令牌的API調用中添加cookie。為此,cookie需要有適當的設置,比如SameSite=Strict、指向API端點域的域屬性和路徑。
最后,在使用刷新令牌時,請確保將它們存儲在自己的cookie中。沒有必要在每個API請求中都發送它們,所以請確保不是這種情況。刷新令牌必須只在刷新過期的訪問令牌時添加。這意味著包含刷新令牌的cookie與包含訪問令牌的cookie有稍微不同的設置。
令牌處理程序模式
在JavaScript客戶端中為OAuth提供最佳實踐原則的設計模式是令牌處理程序模式。它遵循OAuth 2.0 for Browser-Based Apps中描述的BFF(backend for frontend)方法。該模式引入了一個后端組件,能夠發出帶有加密令牌和上述必要屬性的cookie。
后端組件的責任是:
- 作為OAuth客戶端與授權服務器交互,啟動用戶認證并獲取令牌。
- 管理JavaScript應用程序的令牌,使其不可訪問。
- 代理和攔截所有API請求,以附加正確的訪問令牌。
令牌處理程序模式定義了一個BFF,它為在瀏覽器中運行的應用程序抽象了OAuth。換句話說,令牌處理程序模式建議一個JavaScript應用程序可以用來認證用戶并安全地調用API的API。為此,該模式使用cookie來存儲和發送訪問令牌。
令牌處理程序是一個后端組件,例如可以駐留在API網關中。它由兩部分組成:
- OAuth代理,它處理OAuth流以從授權服務器獲取令牌。
- OAuth代理,它攔截對API的所有請求并將cookie轉換為令牌。
圖片
OAuth代理獲取令牌后,它會發出帶有以下屬性的cookie:
- SameSite=Strict
- HttpOnly
- Secure
- API的路徑
由于令牌處理程序是一個后端組件,所以OAuth代理是一個保密的客戶端,可以向授權服務器進行身份驗證(與公開的JavaScript客戶端相比)。這意味著為了獲得令牌,OAuth代理需要進行身份驗證。因此,攻擊者需要獲取客戶端憑據才能成功獲取新令牌。在JavaScript中運行靜默流而沒有客戶端憑據將失敗。
為了令牌處理程序模式能夠工作,JavaScript應用程序和令牌處理程序組件必須部署在同一站點上(換句話說,它們必須在同一域中運行)。否則,由于cookie上的同站限制,瀏覽器不會將令牌cookie添加到API請求中。
要獲取數據,JavaScript應用程序只需通過OAuth代理調用API:
// http://www.example.com/app.js
// Call to OAuth Proxy
const response = await fetch("https://api.example.com/orders", {
// Instruct the browser to add cookies to cross-origin requests
credentials: "include"
});
瀏覽器會自動將cookie添加到請求中。在上面的示例中,瀏覽器將cookie包含在跨域請求中。但是,由于cookie屬性SameSite=Strict,瀏覽器只會將cookie添加到同一站點(同一域)的跨域請求中。
OAuth代理解密cookie并將令牌添加到上游API。cookie屬性確保瀏覽器僅將cookie添加到HTTPS請求中,以確保它們在傳輸過程中是安全的。由于令牌是加密的,它們在休息時也是安全的。然后令牌用于安全訪問API。
總結
使用OAuth和訪問令牌可以最好地保護API訪問。但是,JavaScript應用程序處于不利地位。瀏覽器中沒有安全的令牌存儲解決方案。所有可用的解決方案在某種程度上都容易受到XSS攻擊。因此,確保任何應用程序安全的首要任務應該是防止XSS漏洞。
令牌處理程序模式通過在JavaScript無法訪問的cookie中存儲加密令牌來緩解XSS風險。它將Web關注點與API關注點分離,并提供指導,使用成熟的Web技術加固JavaScript應用程序,而不會破壞Web架構。查看令牌處理程序模式的詳細描述,并探索各種示例。