Надежно установленное неизвестное свойство (предотвращение атак путем внедрения объекта в квадратные скобки) служебная функция
После настройки eslint-plugin-security
, Я попытался рассмотреть около 400 случаев использования квадратных скобок в нашей кодовой базе javascript (отмеченных правилом 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
всегда int, это, очевидно, всегда безопасно.
Одна из ситуаций, я не думаю, что обработали отлично, квадратные скобки назначения, как это:
someObject[somePropertyPotentiallyDefinedFromBackend] = someStringPotentiallyMaliciouslyDefinedString
Статус-кво для Stackru состоит в том, чтобы "показать мне весь код" - когда вы просматриваете базу кода и исправляете ее в сотнях и сотнях экземпляров, требуется гораздо больше времени, чтобы пойти и прочитать код, который предшествует одному из этих назначений.. Кроме того, мы хотим, чтобы этот код оставался безопасным, поскольку он будет изменен в будущем.
Как мы можем убедиться, что устанавливаемое свойство по существу еще не определено для ванильных объектов? (т.е.constructor
)
Пытался решить это сам, но не хватало некоторых деталей. Со временем это будет отредактировано, но оставлено здесь для контекста.
Поэтому я думаю, что проще всего решить эту проблему с помощью простой утилиты, 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;
}
};
})();
Мы также могли бы написать утилиту safeSet
- вместо этого:
obj[key] = value;
Вы бы сделали это:
safeSet(obj, key, value)
Тесты на safeKey (неуспешные):
console.log(safeKey('toString'));
// Good: => SAFE_toString
console.log(safeKey('__proto__'));
// Good: => SAFE___proto__
console.log(safeKey('constructor'));
// Good: => SAFE_constructor
console.log(safeKey('prototype'));
// Fail: => prototype
console.log(safeKey('toJSON'));
// Fail: => toJSON
Затем вы использовали бы это так:
someObject[safeKey(somePropertyPotentiallyDefinedFromBackend)] = someStringPotentiallyMaliciouslyDefinedString
Это означает, что если серверная часть случайно отправит json с ключом где-то constructor
мы не подавляемся этим, а просто используем ключ SAFE_constructor
(лол). Также применяется для любого другого предопределенного метода / свойства, поэтому теперь бэкэнд не должен беспокоиться о конфликте ключей json с собственно определенными свойствами / методами js.
Эта служебная функция - ничто без серии пройденных модульных тестов. Как я уже сказал, не все тесты проходят. Я не уверен, какие объекты изначально определяют toJSON - а это означает, что он может быть частью жестко запрограммированного списка имен методов / свойств, которые должны быть внесены в черный список. Но я не уверен, как узнать каждый из этих методов свойств, которые нужно занести в черный список. Поэтому нам нужно знать, как лучше всего создать этот список и постоянно обновлять его.
Я обнаружил, что использование Object.freeze(Object.prototype) помогает, но такие методы, как toJSON, я не думаю, что существуют в прототипе.
Вот еще один небольшой тестовый пример:
const prop = 'toString';
someData[safeKey(prop)] = () => {
alert('hacked');
return 'foo';
};
console.log('someProp.toString()', someData + '');
Связано: все способы оценки строки javascript: https://www.everythingfrontend.com/posts/studying-javascript-eval.html Я отправил твит автору, упомянувconstructor
лазейка.
1 ответ
Более важно предотвратить доступ к ключу на неправильном объекте, чем проверять / защищать сами ключи объекта. Обозначение определенных ключей объекта как "небезопасных" и недопущение доступа к ним независимо от обстоятельств - всего лишь еще одна форма анти-паттерна "санация". Если объект изначально не содержит конфиденциальных данных, нет риска его отфильтровывания или изменения ненадежными входными данными. Вам не нужно беспокоиться о доступеsrc
или innerHTML
если вы не обращаетесь к нему на узле DOM; вам не нужно беспокоиться о разоблаченииeval
если вы не выполняете поиск по глобальному объекту. Как таковой:
- Используйте скобки только для объектов, которые либо являются массивами, либо специально используются для хранения произвольных сопоставлений "ключ-значение" и ничего другого, особенно без методов (последний тип объектов я собираюсь назвать подобным карте ниже). Для таких объектов также обеспечьте следующее:
- что значения, которые вы назначаете в объекте, похожем на карту, никогда не являются функциями (чтобы избежать проблем с
toJSON
ключ или что-то подобное); - что вы никогда не конвертируете объекты, подобные карте, в строки напрямую, используя
toString
метод,String
конструктор, приведение типа,Array.prototype.join
или другой встроенный механизм (чтобы избежать проблем сtoString
ключ); если вам когда-нибудь понадобится преобразовать объекты, подобные карте, в строки, напишите функцию, явно созданную для этой цели, не добавляя ее как метод к объекту.
- что значения, которые вы назначаете в объекте, похожем на карту, никогда не являются функциями (чтобы избежать проблем с
- При доступе к массивам убедитесь, что индекс действительно является целым числом. Также рассмотрите возможность использования встроенных методов, таких как
push
,forEach
,map
илиfilter
которые вообще избегают явного индексирования; это уменьшит количество мест, которые вам нужно будет проверять. - Если вам когда-нибудь понадобится нарушить указанное выше правило и использовать обозначение скобок с объектами с относительно фиксированным набором ключей, например узлами DOM,
window
или объект, который вы определили с помощьюclass
(все это я буду называть классовым ниже), либо убедитесь, что ключи получены из надежных источников, либо отфильтруйте / проверьте их.- В частности, если вы хотите связать объект, подобный классу, с данными, время жизни которых должно быть привязано к этому объекту, либо используйте
WeakMap
или (если он недоступен) поместите его на ключ под вашим контролем. (Если у вас есть несколько таких данных, которые вы хотите сохранить вместе, вы можете поместить их в объект, похожий на карту, который сам хранится на ключе, управляемом вами.)
- В частности, если вы хотите связать объект, подобный классу, с данными, время жизни которых должно быть привязано к этому объекту, либо используйте
К сожалению, как вы заметили, цепочка прототипов (в частности, Object.prototype
) здесь несколько усложняется: он определяет дескрипторы свойств, которые по умолчанию доступны для любого объекта. Особое беспокойство вызываютconstructor
и различные встроенные методы (которые можно использовать для доступа к Function
объект и, в конечном итоге, выполнить произвольный код) и __proto__
(который можно использовать для изменения цепочки прототипов объекта).
Ниже я перечисляю некоторые стратегии, которые вы можете использовать, чтобы смягчить проблему конфликта произвольных строковых ключей со встроенными в Object.prototype
. Они не исключают друг друга, но для единообразия может быть предпочтительнее придерживаться только одного.
Просто преобразуйте все ключи: это, вероятно, (концептуально) самый простой вариант, переносимый даже для движков со времен ECMAScript 3 и устойчивый даже к будущим дополнениям к
Object.prototype
(как бы маловероятно они ни были). Просто добавьте один символ, не являющийся идентификатором, ко всем ключам в объектах, подобных карте; это безопасно удалит ненадежные ключи от всех разумно мыслимых встроенных модулей JavaScript (которые предположительно должны иметь имена, которые являются действительными идентификаторами). При доступе к объектам, похожим на карту, проверьте наличие этого символа и удалите его соответствующим образом. Следование этой стратегии даже заставит задуматься о таких методах, какtoJSON
илиtoString
в основном не имеет значения. Недостатком этого подхода является то, что символ префикса окажется в JSON, если вы когда-нибудь сериализуете такой объект, похожий на карту.Принудительное выполнение доступа к свойствам непосредственно на объекте. Доступ для чтения можно защитить с помощью
Object.prototype.hasOwnProperty
, что остановит кражу уязвимостей, но не защитит вас от случайной записи в__proto__
. Если вы никогда не мутируете такой объект, похожий на карту, это не должно быть проблемой. Вы даже можете обеспечить неизменность, используяObject.seal
.Однако, если вы настаиваете, вы можете выполнять запись свойств через
Object.defineProperty
, доступный с ECMAScript 5, который может создавать свойства непосредственно на объекте, без использования геттеров и сеттеров.Короче говоря, вы можете просто проксировать доступ ко всем свойствам через эти две функции:
function rawget(obj, key) {
if (Object.prototype.hasOwnProperty.call(obj, key))
return obj[key];
}
function rawset(obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
writable: true,
enumerable: true,
configurable: true
});
return val;
}
Убрать
Object.prototype
: убедитесь, что объекты, похожие на карту, имеют пустую цепочку прототипов. Создайте их черезObject.create(null)
(доступно с ECMAScript 5) вместо{}
. Если вы раньше создавали их с помощью литералов прямых объектов, вы можете обернуть их вObject.assign(Object.create(null), { /* ... */ })
(Object.assign
доступен начиная с ECMAScript 6, но легко подключается к более ранним версиям). Если вы последуете этому подходу, вы можете использовать скобки как обычно; единственный код, который вам нужно проверить, это то, где вы создаете объект, похожий на карту.К сожалению, объекты, созданные
JSON.parse
по умолчанию все равно будет наследовать отObject.prototype
(хотя современные движки, по крайней мере, добавляют ключ JSON, например__proto__
непосредственно на самом созданном объекте, минуя сеттер из дескриптора прототипа). Вы можете обрабатывать такие объекты как доступные только для чтения и защищать доступ для чтения с помощьюhasOwnProperty
(как указано выше), или удалите их прототипы, написав функцию оживления, которая вызываетObject.setPrototypeOf
. Функция оживления также может использоватьObject.seal
чтобы сделать объект неизменным.Использовать
Map
s вместо объектов, похожих на карту: использованиеMap
(доступно с ECMAScript 6) позволяет использовать ключи, отличные от строк, что невозможно с простыми объектами; но даже со строковыми ключами вы получаете то преимущество, что записи карты полностью изолированы от цепочки прототипов самого объекта карты. Пункты вMap
s доступны для.get
а также.set
методы вместо обозначения скобок и вообще не могут конфликтовать со свойствами: ключи существуют в отдельном пространстве имен.Однако есть проблема в том, что
Map
не может быть напрямую сериализован в JSON. Вы можете исправить это, написав функцию замены дляJSON.stringify
что обращаетMap
s в простые объекты, подобные карте, без прототипов, и функцию оживления дляJSON.parse
что превращает простые предметы обратно вMap
с. Опять же, наивное оживление каждого объекта JSON вMap
также будет охватывать объекты, подобные классам, которые вам, вероятно, не нужны. Чтобы различать их, вам может потребоваться добавить какой-то параметр схемы в функцию анализа JSON.
Если вы спросите меня о моих предпочтениях: используйте Map
если вам не нужно беспокоиться о движках до ES6 или сериализации JSON; в противном случае используйтеObject.create(null)
; и если вам нужно работать с устаревшими движками JS, где ни то, ни другое невозможно, преобразуйте все ключи (первый вариант) и надейтесь на лучшее.
Можно ли механически обеспечить соблюдение всей этой дисциплины? Я не знаю инструмента, который бы это делал. Предположительно, такой инструмент позволил бы аннотировать переменные как содержащие объекты, подобные карте, для которых разрешен доступ в скобки, и, вероятно, также разрешил бы аннотировать свойства других объектов, чтобы также были охвачены подобные карты объекты, не содержащиеся непосредственно в переменных. Возможно, следует даже распространять такие аннотации через вызовы функций. Он также должен гарантировать, что такие объекты, подобные карте, построены должным образом без прототипа. На этом этапе вы в значительной степени пишете средство проверки типов, так что, возможно, стоит попробовать TypeScript; то, тем не менее, я не знаю, способен ли TypeScript на это.