簡體   English   中英

如何減輕 JavaScript 中動態屬性訪問(即方括號表示法)的注入/滲漏攻擊?

[英]How can I mitigate injection/exfiltration attacks from dynamic property accesses (i.e. square bracket notation) in JavaScript?

在設置eslint-plugin-security ,我繼續嘗試解決我們 JavaScript 代碼庫中近 400 次方括號的使用(由規則 security/detect-object-injection 標記)。 雖然這個插件可以更智能,但任何方括號的使用都可能成為惡意代理注入自己代碼的機會。

要了解如何以及了解我的問題的整個上下文,您需要閱讀此文檔: https : //github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square -bracket-notation.md

我通常嘗試使用Object.prototype.hasOwnProperty.call(someObject, someProperty)來減少someProperty被惡意設置為constructor的可能性。 很多情況只是在 for 循環中取消引用數組索引( for (let i=0;i<arr.length;i++) { arr[i] } )如果i總是一個數字,這顯然總是安全的。

我認為我沒有完美處理的一種情況是這樣的方括號分配

someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString

我認為解決這個問題的最簡單方法是使用一個簡單的 util, safeKey定義如下:

// use window.safeKey = for easy tinkering in the console.
const safeKey = (() => {
  // Safely allocate plainObject's inside iife
  // Since this function may get called very frequently -
  // I think it's important to have plainObject's
  // statically defined
  const obj = {};
  const arr = [];
  // ...if for some reason you ever use square brackets on these types...
  // const fun = function() {}
  // const bol = true;
  // const num = 0;
  // const str = '';
  return key => {
    // eslint-disable-next-line security/detect-object-injection
    if (obj[key] !== undefined || arr[key] !== undefined
      // ||
      // fun[key] !== undefined ||
      // bol[key] !== undefined ||
      // num[key] !== undefined ||
      // str[key] !== undefined
    ) {
      return 'SAFE_'+key;
    } else {
      return key;
    }
  };
})();

然后你會像這樣使用它:

someObject[safeKey(somePropertyPotentiallyDefinedFromBackend)] = someStringPotentiallyMaliciouslyDefinedString

這意味着如果后端偶然發送帶有constructor某處密鑰的 JSON,我們不會阻塞它,而只是使用密鑰SAFE_constructor (lol)。 也適用於任何其他預定義的方法/屬性,因此現在后端不必擔心 JSON 鍵與本機定義的 JS 屬性/方法發生沖突。

如果沒有一系列通過的單元測試,這個實用函數就毫無意義。 正如我所評論的,並不是所有的測試都通過了。 我不確定哪些對象本身定義了toJSON - 這意味着它可能需要成為必須列入黑名單的方法/屬性名稱的硬編碼列表的一部分。 但我不確定如何找出需要列入黑名單的一種屬性方法。 所以我們需要知道任何人都可以生成這個列表的最佳方式,並保持更新。

我確實發現使用Object.freeze(Object.prototype)幫助,但我認為原型中不存在像toJSON這樣的方法。

我們如何確保設置的屬性基本上尚未在 vanilla 對象上定義? (即constructor

防止密鑰在錯誤的對象上被訪問比驗證/保護對象密鑰本身更重要 將某些對象鍵指定為“不安全”並避免在任何情況下只訪問這些鍵只是“消毒”反模式的另一種形式。 如果對象首先不包含敏感數據,則不存在被不可信輸入竊取或修改的風險。 如果不在 DOM 節點上訪問,則無需擔心訪問srcinnerHTML 如果不對全局對象執行查找,則無需擔心暴露eval 像這樣:

  • 僅對數組或特別包含從任意字符串到其他值的映射(通常由對象文字表示法構造的那些)的對象使用方括號表示法; 這種對象我將在下面稱為類似地圖的對象。 使用類似地圖的對象時,還要確保以下幾點:
    • 你永遠不會將函數(或者,在 ECMAScript 的更高版本中,類或代理)直接存儲在類似地圖的對象中。 這是為了避免在諸如'toJSON''then''toJSON'鍵映射到語言可能隨后將其解釋為修改對象行為的方法的函數時出現問題。 如果出於某種原因,您需要將函數存儲在類似地圖的對象中,請將函數放入類似{ _: function () { /* ... */ } }的包裝器中。
    • 永遠不要使用內置語言機制將類似映射的對象強制轉換為字符串: toString方法、 String構造函數(帶或不帶new )、 +運算符或Array.prototype.join 這是為了避免在類似地圖的對象上設置'toString'鍵時觸發問題,因為即使是非函數值也會阻止默認強制行為的發生,而是會拋出TypeError
  • 訪問數組時,請確保索引確實是整數。 還可以考慮使用內置方法,如pushforEachmapfilter ,它們完全避免了顯式索引; 這將減少您需要審核的地方數量。
  • 如果您需要將任意數據與具有一組相對固定鍵的對象相關聯,例如 DOM 節點、 window或您使用class定義的對象(我將在下面將所有這些稱為類類),並且出於某種原因WeakMap不可用,把它的數據放在一個硬編碼的鍵上; 如果您有多個這樣的數據項,請將其放入存儲在硬編碼鍵上的類似地圖的對象中。

即使按照上述操作,您仍然可能因無意中訪問Object.prototype的屬性而成為注入或滲漏攻擊的Object.prototype 特別令人擔憂的是constructor和各種內置方法(可用於訪問Function對象,並最終執行任意代碼執行)和__proto__ (可用於修改對象的原型鏈)。 為了防范這些威脅,您可以嘗試以下一些策略。 它們並不相互排斥,但為了一致性起見,最好只使用一個。

  • Mangle all keys :這可能是(概念上)最簡單的選項,甚至可以移植到 ECMAScript 3 時代的引擎,並且即使將來添加到Object.prototype也很Object.prototype (盡管它們不太可能)。 只需在類地圖對象中的所有鍵前添加一個非標識符字符即可; 這將安全地從所有可以合理想象的 JavaScript 內置函數(大概應該具有有效標識符的名稱)中安全地命名空間遠離不受信任的鍵。 訪問類似地圖的對象時,請檢查此字符並根據需要對其進行剝離。 遵循此策略甚至會使對toJSONtoString類的方法的擔憂幾乎無關緊要。

     // replacement for (key in maplike) function maplikeContains(maplike, key) { return ('.' + key) in maplike; } // replacement for (maplike[key]) function maplikeGet(maplike, key) { return maplike['.' + key]; } // replacement for (maplike[key] = value) function maplikeSet(maplike, key, value) { return maplike['.' + key] = value; } // replacement for the for-in loop function maplikeEach(maplike, visit) { for (var key in maplike) { if (key.charAt(0) !== '.') continue; if (visit(key.substr(1), maplike[key])) break; } }

    不加選擇地修改所有鍵可確保您最終不會將未修改的鍵與損壞的鍵混淆,反之亦然。 例如,如果像在問題中一樣,您將'constructor''SAFE_constructor' ,但將'SAFE_constructor'本身保留'SAFE_constructor' ,那么在修改兩個鍵后最終將引用相同的數據,這可能是本身。

    這種方法的一個缺點是前綴字符將在 JSON 中結束,如果你曾經序列化過這樣一個類似地圖的對象。

  • 強制直接訪問屬性 可以使用Object.prototype.hasOwnProperty保護讀取訪問,這將阻止Object.prototype.hasOwnProperty漏洞,但不會防止您無意中寫入__proto__ 如果你從不改變這樣一個類似地圖的對象,這應該不是問題。 您甚至可以使用Object.seal強制執行不變性。 如果不想這樣,您可以通過Object.defineProperty執行屬性寫入,從 ECMAScript 5 開始可用,它可以直接在對象上創建屬性,繞過 getter 和 setter。

     // replacement for (key in maplike) function maplikeContains(maplike, key) { return Object.prototype.hasOwnProperty.call(maplike, key); } // replacement for (maplike[key]) function maplikeGet(maplike, key) { if (Object.prototype.hasOwnProperty.call(maplike, key)) return maplike[key]; } // replacement for (maplike[key] = value) function maplikeSet(maplike, key, value) { Object.defineProperty(maplike, key, { value: value, writable: true, enumerable: true, configurable: true }); return value; } // replacement for the for-in loop function maplikeEach(maplike, visit) { for (var key in maplike) { if (!Object.prototype.hasOwnProperty.call(maplike, key)) continue; if (visit(key, maplike[key])) break; } }
  • 清除原型鏈:確保類似地圖的對象有一個空的原型鏈。 通過Object.create(null) (從 ECMAScript 5 開始可用)而不是{}創建它們。 如果您之前通過直接對象字面量創建它們,您可以將它們包裝在Object.assign(Object.create(null), { /* ... */ })Object.assign自 ECMAScript 6 起可用,但很容易 shimmable早期版本)。 如果你遵循這種方法,你可以像往常一樣使用括號表示法; 您需要檢查的唯一代碼是您構造類似地圖的對象的位置。

    默認情況下,由JSON.parse創建的對象仍將繼承自Object.prototype (盡管現代引擎至少會直接在構造的對象本身上添加像__proto__這樣的 JSON 鍵,繞過原型描述符中的 setter)。 您可以將此類對象視為只讀對象,並通過hasOwnProperty (如上所述)保護讀取訪問,或者通過編寫調用Object.setPrototypeOf的 reviver 函數來剝離它們的原型。 reviver 函數還可以使用Object.seal使對象不可變。

     function maplikeNew(maplike) { return Object.assign(Object.create(null), maplike); } function jsonParse(json) { return JSON.parse(json, function (key, value) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) Object.setPrototypeOf(value, null); return value; }); }
  • 使用Map代替類似地圖的對象:使用Map (自 ECMAScript 6 起可用)允許您使用字符串以外的鍵,這對於普通對象是不可能的; 但即使只是使用字符串鍵,您也可以享受地圖條目與地圖對象本身的原型鏈完全隔離的好處。 Map的項目通過.get.set方法而不是括號表示法訪問,並且根本不會與屬性沖突:鍵存在於單獨的命名空間中。

    但是有一個問題,一個Map不能直接序列化成 JSON。 您可以通過為JSON.stringify編寫一個JSON.stringify函數來解決這個問題,該函數將Map s 轉換為普通的、無原型的類似 map 的對象,並為JSON.parse編寫一個 reviver 函數,將普通對象轉換回Map s。 再說一次,將每個JSON 對象天真地恢復為Map也將涵蓋我在上面稱為“類類”的結構,您可能不想要這種結構。 要區分它們,您可能需要向 JSON 解析函數添加某種架構參數。

     function jsonParse(json) { return JSON.parse(json, function (key, value) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) return new Map(Object.entries(value)); return value; }); } function jsonStringify(value) { return JSON.stringify(value, function (key, value) { if (value instanceof Map) return Object.fromEntries(value.entries()); return value; }); }

如果您問我的偏好:如果您不需要擔心 ES6 之前的引擎或 JSON 序列化,請使用Map 否則使用Object.create(null) 如果您需要使用兩種都不可能的遺留 JS 引擎,請修改鍵(第一個選項)並希望最好。

現在,所有這些紀律都可以機械地執行嗎? 是的,它被稱為靜態類型。 有了足夠好的類型定義,TypeScript 應該能夠捕獲以類映射方式訪問類類對象的情況,反之亦然。 它甚至可以捕獲出現具有不需要的原型的對象的某些情況:

type Maplike<T> = {
    [K: string]: T|undefined;
    constructor?: T;
    propertyIsEnumerable?: T;
    hasOwnProperty?: T;
    toString?: T;
    toLocaleString?: T;
    valueOf?: T;
};

const plain: { [K: string]: string } = {};

const naked: Maplike<string> = Object.create(null);   // OK
const error: Maplike<string> = {};                    // type error

function f(k: string) {
    naked[k] = 'yay';                                 // OK
    plain[k] = 'yay';                                 // OK
    document.body[k] = 'yay';                         // type error
}

console.info(plain.toString());                       // OK
console.info(naked.toString());                       // type error

但是請記住,這不是靈丹妙葯。 上面的類型定義可能會發現最明顯的錯誤,但是想出一個它不會檢測到的情況並不難。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM