擴充功能可存取瀏覽器中的特殊權限,因此對攻擊者來說是極具吸引力的目標。如果擴充功能遭到入侵,所有使用者都會受到惡意和不當入侵的威脅。請採用下列做法,確保擴充功能安全無虞,並保護使用者。
保護開發人員帳戶
擴充功能程式碼會透過 Google 帳戶上傳及更新。如果開發人員的帳戶遭到入侵,攻擊者可能會直接將惡意程式碼推送給所有使用者。請建立專屬的開發人員帳戶,並啟用雙重驗證 (最好使用安全金鑰),保護這些帳戶。
選擇要保留的群組
如果使用群組發布功能,請只允許信任的開發人員加入群組。請勿接受陌生人的成員要求。
一律不使用 HTTP
要求或傳送資料時,請避免使用 HTTP 連線。請假設任何 HTTP 連線都會遭到竊聽或修改。HTTPS 內建安全機制,可規避大多數中間人攻擊,因此應優先使用。
要求最少的權限
Chrome 瀏覽器會限制擴充功能只能存取資訊清單中明確要求的權限。擴充功能應只註冊所依附的 API 和網站,盡量減少權限。應盡量減少任意程式碼。
限制擴充功能的權限,可限制潛在攻擊者可利用的項目。
跨來源 XMLHttpRequest
擴充功能只能使用 XMLHttpRequest 從自身和權限中指定的網域取得資源。
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"permissions": [
"/*",
"https://*.google.com/"
],
"manifest_version": 2
}
這項擴充功能要求存取 developer.chrome.com 和 Google 子網域的任何內容,方法是在權限中列出 "/*" 和 "https://*google.com/"。即使擴充功能遭到入侵,也只會獲得與符合相符模式的網站互動的權限。攻擊者無法存取 "https://user_bank_info.com" 或與 "https://malicious_website.com" 互動。
限制資訊清單欄位
在資訊清單中加入不必要的註冊項目會產生安全漏洞,並讓擴充功能更容易遭到攻擊。將資訊清單欄位限制為擴充功能所依附的欄位,並提供特定欄位註冊。
可連至外部網站
使用 externally_connectable 欄位宣告擴充功能會與哪些外部擴充功能和網頁交換資訊。限制擴充功能可與哪些外部對象連線,確保連線對象為信任來源。
{
"name": "Super Safe Extension",
"externally_connectable": {
"ids": [
"iamafriendlyextensionhereisdatas"
],
"matches": [
"/*",
"https://*google.com/"
],
"accepts_tls_channel_id": false
},
...
}
可透過網路存取的資源
如果資源可透過網路存取 (位於 web_accessible_resources 下),網站和攻擊者就能偵測到擴充功能。
{
...
"web_accessible_resources": [
"images/*.png",
"style/secure_extension.css",
"script/secure_extension.js"
],
...
}
可透過網路存取的資源越多,潛在攻擊者可利用的途徑就越多。盡量減少這類檔案。
加入明確的內容安全政策
在資訊清單中加入擴充功能的內容安全政策,防範跨網站指令碼編寫攻擊。如果擴充功能只會載入自身的資源,請註冊下列項目:
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"content_security_policy": "default-src 'self'"
"manifest_version": 2
}
如果擴充功能需要納入特定主機的指令碼,可以採取下列做法:
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"content_security_policy": "default-src 'self' https://extension.resource.com"
"manifest_version": 2
}
避免使用可執行的 API
請改用較安全的替代方案,取代會執行程式碼的 API。
document.write() 和 innerHTML
雖然使用 document.write() 和 innerHTML 動態建立 HTML 元素可能比較簡單,但這會讓擴充功能和擴充功能所依附的網頁,容易遭到攻擊者插入惡意指令碼。請改為手動建立 DOM 節點,並使用 innerText 插入動態內容。
function constructDOM() {
let newTitle = document.createElement('h1');
newTitle.innerText = host;
document.appendChild(newTitle);
}
eval()
請盡量避免使用 eval(),以免遭受攻擊,因為 eval() 會執行傳遞給它的任何程式碼,其中可能含有惡意程式碼。
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// WARNING! Might be evaluating an evil script!
var resp = eval("(" + xhr.responseText + ")");
...
}
}
xhr.send();
建議改用更安全快速的方法,例如 JSON.parse()
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// JSON.parse does not evaluate the attacker's scripts.
var resp = JSON.parse(xhr.responseText);
}
}
xhr.send();
謹慎使用內容指令碼
- 內容指令碼是擴充功能中唯一能直接與網頁互動的部分。 因此,惡意網頁可能會操控內容指令碼所依附的 DOM 部分,或利用令人意外的網頁標準行為,例如具名項目。
- 如要與網頁的 DOM 互動,內容指令碼必須在與網頁相同的轉譯器程序中執行。這會導致內容指令碼容易透過旁路攻擊 (例如 Spectre) 外洩資料,且如果惡意網頁入侵了轉譯器程序,攻擊者就能接管內容指令碼。
敏感作業應在專屬程序中執行,例如擴充功能的背景指令碼。避免不慎將擴充功能權限暴露給內容指令碼:
- 假設來自內容指令碼的訊息可能由攻擊者製作 (例如驗證及清除所有輸入內容,並保護指令碼免於跨網站指令碼攻擊)。
- 假設傳送至內容指令碼的任何資料都可能洩漏至網頁。請勿將機密資料 (例如擴充功能的密碼、其他網頁來源的資料、瀏覽記錄) 傳送至內容指令碼。
- 限制可由內容指令碼觸發的權限動作範圍。請勿允許內容指令碼觸發任意網址的要求,或將任意引數傳遞至擴充功能 API (例如,請勿允許將任意網址傳遞至
fetch或chrome.tabs.createAPI)。
註冊及清理輸入內容
限制監聽器只能監聽擴充功能預期的內容、驗證傳入資料的傳送者,以及清除所有輸入內容,藉此防範惡意指令碼。
如果擴充功能預期會收到來自外部網站或擴充功能的通訊內容,才應註冊 runtime.onRequestExternal。請務必確認寄件者是否為信任來源。
// The ID of an external extension
const kFriendlyExtensionId = "iamafriendlyextensionhereisdatas";
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id === kFriendlyExtensionId)
doSomething();
});
即使是來自擴充功能本身的 runtime.onMessage 事件訊息,也應仔細檢查,確保 MessageSender 不是來自遭入侵的內容指令碼。
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.allowedAction)
console.log("This is an allowed action.");
});
即使是來自擴充功能本身和核准來源,也請清除使用者輸入內容和傳入資料,防止擴充功能執行攻擊者的指令碼。避免使用可執行的 API。
function sanitizeInput(input) {
return input.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
}