Практика прототипирования в софтверной компании / Хабр

Практика прототипирования в софтверной компании / Хабр Сертификаты
Содержание
  1. Что такое prototype pollution?
  2. Json схема
  3. Object.create()
  4. Object.create(null)
  5. Object.freeze()
  6. Prototype pollution на клиенте
  7. Prototype pollution на сервере
  8. Благодарности
  9. В чем отличие __proto__ от prototype
  10. Динамический анализ
  11. Жизненный цикл прототипа
  12. Защита
  13. Зри в корень. ищи ответы
  14. Изменение прототипа
  15. Импакт
  16. Импакт и поиск гаджетов
  17. Использование существующих гаджетов
  18. Как мы делаем прототипы?
  19. Как образец при приёмке-сдаче работ
  20. Как образец при тестировании готового по
  21. Как получить прототип объекта?
  22. Как пример решения для демонстрации потенциальным заказчикам
  23. Какие конструкции подвержены уязвимости?
  24. Какие прототипы мы используем?
  25. Ключевое слово new
  26. Когда и как использовать прототипы? теории и практики
  27. Кто должен прототипировать?
  28. На этапе коммерческого предложения
  29. Необычный кейс
  30. Объект
  31. Особенности javascript
  32. Переходим к классам
  33. Последствия прототипирования
  34. Прототипирование на постсоветском пространстве
  35. Прототипировать ли?
  36. Прототипное наследование
  37. Прототипы бывают разные…
  38. Свойство __proto__
  39. Ссылки
  40. Статический анализ
  41. Субклассирование
  42. Функции – тоже объекты
  43. Цифры
  44. Черный список полей
  45. Итоги
  46. Вывод
  47. Промежуточные итоги. выжимка из теории

Что такое prototype pollution?

Термином prototype pollution называют ситуацию, когда изменяют свойство prototypeбазовых объектов (например, Object.prototype, Function.prototype, Array.prototype).

> Object.prototype.age = 42
< 42
> var a = []
< undefined
> a.age
< 42

После исполнения этого кода практически у любого объекта будет свойство age со значением 42. Исключением является два случая:

Как prototype pollution может выглядеть в коде? Рассмотрим программу pp.js.

$ cat pp.js
var o = {};
var a = process.argv[2];
var b = 'test';
var v = process.argv[3];

console.log(({}).test);

o[a][b] = v;

console.log(({}).test);

Если злоумышленник контролирует параметры a и v, то он может установить a в значение ‘__proto__’ и v в произвольное строковое значение, таким образом добавив свойство test на Object.prototype.

$ node pp.js __proto__ 123
undefined
123

Поздравляю, мы только что нашли prototype pollution! “Но кто в здравом уме будет использовать подобные конструкции?” — спросите вы. Действительно, данный пример редко встретишь в реальной жизни. Однако существуют конструкции, на первый взгляд безобидные, которые при определенных обстоятельствах позволяют нам добавлять или изменять свойства Object.prototype. Конкретные примеры мы разберем в следующих разделах.

Json схема

Можно валидировать входные данные на соответствие заранее определенной JSON схеме и отбрасывать все остальные параметры. Например, это можно сделать с помощью библиотеки avj с параметром additionalProperties = false.

Object.create()

function DogObject(name, age) {
    let dog = Object.create(constructorObject);
    dog.name = name;
    dog.age = age;
    return dog;
}
let constructorObject = {
    speak: function(){
        return "I am a dog"
    }
}
let bingo = DogObject("Bingo", 54);
console.log(bingo);

Посмотрим на объект bingo в консоли:

Теперь в свойстве __proto__ находится другая функция-конструктор и метод speak.

Object.create создает новый объект, автоматически устанавливая ему указанный прототип.

Object.create(null)

Можно использовать объект без прототипа, тогда модификация прототипа будет невозможна.

> var o = Object.create(null)
< undefined
> o.__proto__
< undefined

Минус в том, что дальше этот объект может поломать часть функционала. Например, кто-то захочет вызвать на этом объекте toString() и в ответ получит o.toString is not a function.

Object.freeze()

Еще один вариант это “заморозить” Object.prototype с помощью функции Object.freeze(). После этого Object.prototype нельзя будет модифицировать.

Однако есть несколько подводных камней:

Prototype pollution на клиенте

Клиентский prototype pollution начали активно исследовать в середине 2020 года. На данный момент хорошо исследован вектор, когда пейлод находится в параметрах запроса (после ?) или в фрагменте (после #). Подобная уязвимость чаще всего эскалируется до Reflected XSS.

Вполне возможно, что пейлод можно не только передать в параметрах запроса или фрагменте, но и сохранить на сервере. Таким образом пейлод будет отрабатывать каждый раз и для каждого пользователя, который посетит определенную страницу, вне зависимости от того посетил ли он вредоносную ссылку.

Prototype pollution на сервере

Все началось с исследования Olivier Arteau — Prototype pollution attacks in NodeJS applications, prototype-pollution-nsec18. Оливер обнаружил уязвимость prototype pollution сразу в нескольких npm пакетах, включая один из самых популярных пакетов lodash (CVE-2021-3721).

Пакет lodash используется во многих приложениях и пакетах JavaScript экосистемы. В том числе он применяется в популярной Ghost CMS, которая, из-за этого, была уязвима к удаленному выполнения кода, для эксплуатации уязвимости не требовалась аутентификация.

Благодарности

В первую очередь хочется поблагодарить Olivier, Michał Bentkowski, Sergey Bobrov, s1r1us, po6ix, William Bowling за статьи, доклады и программы по теме prototype pollution, которыми они поделились со всеми. Без них исследование едва бы началось 🙂

Сергею Боброву и Михаилу Егорову за коллаборацию в поиске уязвимостей.

За вычитку, обратную связь и другую помощь по статье спасибо Анатолию Катюшину, Александру Барабанову, Денису Макрушину и Дмитрию Жерегеле.

В чем отличие __proto__ от prototype

Свойство с именем prototype можно указать на любом объекте, но особый смысл оно
имеет, лишь если назначено функции-конструктору.

Само по себе, без вызова оператора new, оно вообще ничего не делает,
его единственное назначение – указывать __proto__ для новых объектов.

Динамический анализ

Начнем с динамического, так как он проще для понимания и применения. Алгоритм довольно простой и уже реализован в find-vuln:

Жизненный цикл прототипа

Теперь, когда перечислены, наверное, все возможные способы использования прототипов, покажу полный цикл использования прототипов у нас.

  1. Установление контакта с потенциальным заказчиком и получение предварительной информации от него. Менеджер создаёт небольшой интерактивный прототип. Сам, пока без привлечения дизайнера. Отправляем коммерческое предложение вместе с видеозаписью прототипа.
  2. Если мы выбраны в качестве исполнителя проекта, то поручаем дизайнеру отрисовать UI-компоненты и дорабатываем прототип. Идём к заказчику с обновлённым прототипом. При этом, если заказчик и пользователи – разные лица, мы просим допустить нас к конечным пользователям: они прямо на прототипе показывают, что им нравится, а что они хотят изменить. Таким образом мы собираем требования, в соответствии с ними изменяем прототип и опять идём к пользователям. За несколько итераций прототип, и, соответственно, функциональность, согласована.
  3. Когда функциональность согласована, мы просим пользователей указать функции, которые им выполнять неудобно, некомфортно, непривычно. Исправляем прототип в соответствиями с замечаниями. Это своего рода юзабилити-тестирование.
  4. Параллельно с общением с пользователем согласовываем прототип и с разработчиками. Узнаём, возможно ли и насколько сложно реализовать то, что показано в прототипе. Если какую-то функцию реализовать невозможно – тогда придумываем альтернативный вариант и согласовываем с заказчиком. В конце концов получаем прототип, согласованный как с заказчиком, так и с разработчиками.
  5. Снимаем с прототипа скриншоты и делаем на их основе ТЗ.
  6. Отдаём ТЗ и прототип разработчикам. Разработчики реализуют систему, используя прототип в качестве образца.
  7. Готовая система и её прототип отдаются тестировщикам. Они также используют прототип в качестве образца.
  8. Сдаем систему заказчику. Он проверяет, соответствует ли реализованная система прототипу.
  9. Прототип уходит в архив. Но если заказчик просит доработку, ты мы достаём прототип и дорабатываем его с учётом новых требований. И дальше вновь по циклу со второго шага.

Защита

Исправить данную уязвимость можно по-разному, начнем с наиболее популярного варианта.

Зри в корень. ищи ответы


Хочу отметить, что инструмент – это всего лишь средство обеспечения процесса прототипирования. Первичен именно процесс. Без построенного и отлаженного процесса ни один инструмент прототипирования «не выстрелит». Сначала нужно ответить на вопросы:

И уже потом решать, с помощью чего это делать. Только тогда к вам придёт понимание того, какой инструмент вам нужен.

Изменение прототипа

Не следует менять свойство __proto__ напрямую, для этого существуют специальные методы.

Импакт

Допустим мы обнаружили библиотеку, уязвимую к prototype pollution. Какой ущерб эта уязвимость может нанести системе?

В NodeJS окружении это практически всегда гарантированный DoS, потому что можно перезаписать базовую функцию (например, Object.prototype.toString()) и все вызовы этой функции будут возвращать исключение. Рассмотрим на примере популярного сервера expressjs/express.

$ cat server.js
var merge = require('merge-deep');
var express = require('express');
var bodyParser = require('body-parser');

var app = express();

app.use(bodyParser.json({
  type: 'application/* json'
}));

app.get('/', function(req, res) {
  res.send("Use the POST method !");
});

app.post('/', function(req, res) {
  merge({}, req.body);
  res.send(req.body);
});

app.listen(3000, function() {
  console.log('Example app listening on port 3000!')
});

Устанавливаем зависимости и запускаем сервер.

$ npm i merge-deep@3.0.2 express body-parser
$ node server.js

И в другой вкладке терминала отправляем пейлод.

Про сертификаты:  Аккредитация сестринское дело с 1-400 вопрос

Импакт и поиск гаджетов

На клиенте больше всего интересна эскалация до XSS. JavaScript код, с помощью которого можно эскалировать prototype pollution до нормальной уязвимости, называется гаджетом. Как правило у нас есть либо известный гаджет, либо мы должны искать гаджеты самостоятельно. Самостоятельный поиск новых гаджетов это дело достаточно трудоемкое.

Использование существующих гаджетов

В первую очередь имеет смысл проверить существующие гаджеты в репозитории BlackFan/client-side-prototype-pollution или в Cross-site scripting (XSS) cheat sheet.

Есть как минимум два способа проверки известных гаджетов:

  1. С помощью плагина Wappalyzer.
  2. С помощью скрипта fingerprint.js.

Как мы делаем прототипы?

Мы поставили перед собой два условия: во-первых, прототипы должны быть интерактивными, во-вторых, прототипировать должны менеджеры. Нам нужен был инструмент, который позволяет создавать интерактивные прототипы без программирования, т.к. менеджеры в общем случае не умеют программировать.

imageGUI Machine. Сейчас это кроссплатформенный инструмент прототипирования, который позволяет создавать интерактивные прототипы декстоп и веб-приложений без программирования.

Использование для создания прототипов собственного инструмента имеет как положительные, так и отрицательные стороны. Минусом для компании является необходимость выделения ресурсов на развитие GUI Machine. С выводом продукта на рынок количество необходимых ресурсов только увеличиваются: инструмент нужно продвигать, развивать, поддерживать.

Как образец при приёмке-сдаче работ

Здесь уже больше используем прототип не мы, а наши заказчики. При приёмке работ они теперь помимо, а иногда и вместо проверки функций по ТЗ, сравнивают реализованную систему с прототипом. Это выгодно обеим сторонам. Заказчик вправе предъявить претензии, если в реализованной системе что-то не так, как в прототипе.

Но и мы как исполнитель можем защитить себя от претензий, если реализуем систему точно так же, как в прототипе. У заказчика просто не будет оснований быть недовольным. Таким образом, исполнитель отдаёт, а заказчик получает ровно то, что было согласовано – ни больше, ни меньше. Никто не делает лишней работы, и все довольны.

Как образец при тестировании готового по

Мы используем прототип в качестве образца не только при реализации системы, но и при её тестировании. У наших тестировщиков сейчас под рукой, помимо ТЗ, всегда есть прототип. Некоторые интерфейсные функции тяжело описываются и воспринимаются в текстовом виде, и в этом случае прототип является хорошим помощником.

Как получить прототип объекта?

via __proto__или Object.getPrototypeOf

var a = { name: "wendi" };
a.__proto__ === Object.prototype // true
Object.getPrototypeOf(a) === Object.prototype // true

function Foo() {};
var b = new Foo();
b.__proto__ === Foo.prototype
b.__proto__.__proto__ === Object.prototype

Как пример решения для демонстрации потенциальным заказчикам


Иногда у нас нет возможности продемонстрировать заказчику готовую систему и мы показываем ему прототипы, разработанные и использованные на прошлых проектах.

Какие конструкции подвержены уязвимости?

Чаще всего prototype pollution находят в следующих конструкциях / операциях:

Мы можем проследить закономерность: уязвимы те операции, которые на вход принимают сложную структуру данных (например, .toml) и преобразуют ее в JavaScript объект.

Какие прототипы мы используем?

Одноразовые прототипы делятся в свою очередь по:

Большинство из нас делает «бумажные» прототипы в самом начала этапа сбора требований. Затем по мере уточнения требований увеличивается точность прототипов. Кто-то останавливается на вайрфрейме (каркас интерфейса без конечного дизайна), кто-то доводит до мокапов с дизайном, близким к конечному.

Но наибольший интерес представляют интерактивные прототипы. Именно они позволят полноценно вовлечь пользователя, показать систему целиком и в действии. Для нас интерактивные прототипы являются наиболее эффективными, поэтому именно их мы и используем в нашей деятельности.

Ключевое слово new

function DogObject(name, age) {
    this.name = name;
    this.age = age;
}
DogObject.prototype.speak = function() {
    return "I am a dog";
}
let john = new DogObject("John", 45);

john.__proto__ === DogObject.prototype; // true

Теперь свойство john.__proto__ ссылается на DogObject.prototype. Обратите внимание – не на функциюDogObject (она является только конструктором объектов), а на объект, записанный в ее свойстве prototype.

В свою очередь прототип прототипа john ссылается на уже знакомый нам Object.prototype.

DogObject.prototype.__proto__ === Object.prototype;

То, что мы видим, называется цепочкой прототипов. Это основа прототипно-ориентированного программирования. Объект может получить доступ к любому свойству или методу, которое есть у любого звена его прототипной цепочки.

Сколько бы прототипов ни было в этой цепочке, последний почти всегда будет наследовать от исходного Object.prototype. При необходимости можно избавиться от дефолтного прототипа, вызвав метод Object.create и передав ему null. Это бывает полезно для создания словарей, в которых не должно быть ничего лишнего.

Ключевое слово new, которое превращает обычную функцию в конструктор объектов, делает по сути ту же самую вещь, что и Object.create(), только проще.

Когда и как использовать прототипы? теории и практики

Для определения места прототипирования в процессе разработки ПО, в первую очередь мы обратились к международным стандартам в этой области и собрали следующую информацию.

Кто должен прототипировать?

Вернёмся к первому опросу. Если смотреть на исполнителей, прототипированием в большинстве проектов занимаются технические специалисты. Для меня это стало неожиданностью: как я уже сказал, большинство стандартов и публикаций рассматривают прототипирование в первую очередь как инструмент для извлечения и утверждения требований.

А кто занимается сбором требований? Менеджер или аналитик, но никак не технический специалист. Если это будет делать он – у нас опять появятся большие потери информации, менеджер будет играть роль сломанного телефона. Поэтому мы считаем, что прототипировать должен именно менеджер, как центральное звено команды проекта и как лицо, непосредственно контактирующее с заказчиком.

На этапе коммерческого предложения

Иногда мы делаем прототипы на этапе коммерческого предложения, т.е. ещё до запуска проекта и до сбора требований, основываясь только на базовой информации от потенциального заказчика. Как мы это делаем – я уже

Для чего мы это делаем? Тут всё просто: прототип позволяет нам выделиться из десятка похожих коммерческих предложений и расположить к себе заказчика.

Мы делаем это не всегда, т.к. всегда есть риск того, что исполнителями проекта выберут не нас, и, соответственно, работы по прототипированию оплачены не будут.

Необычный кейс

Помимо обычных векторов __proto__[polluted]=1337 и __proto__.polluted=31337 однажды я наткнулся на странный кейс. Это было на одном большом сайте. К сожалению репорт еще не раскрыт, поэтому без имени компании. Мой приватный плагин для поиска prototype pollution сообщал об уязвимости, но воспроизвести c помощью обычных векторов ее не удавалось. Я сел разбираться руками в чем же дело. Уязвимость уже исправили, но у нас есть дубликат.

Объект

Как в JavaScript существуют объекты? Откроем инструменты разработчика и создадим простой объект, содержащий два свойства.

> var o = {name: 'Ivan', surname: 'Ivanov'}
< undefined

Мы можем получить доступ к свойствам объекта двумя основными способами.

> o.name
< "Ivan"
> o['surname']
< "Ivanov"

Что будет, если мы попробуем получить доступ к несуществующему свойству?

> o.age
< undefined

Мы получили значение undefined, что означает отсутствие свойства. Пока что ничего необычного.

В JavaScript с функциями можно обращаться как с обычными переменными (подробности в статье функции первого класса), поэтому методы объекта определяются как свойства и по сути ими и являются. Добавим метод foo() на объекте o и вызовем его.

> o.foo = function() {
>   console.log("foobar")
> }
> o.foo()
< foobar
< undefined

Пробуем вызвать метод toString().

> o.toString()
< "[object Object]"

Внезапно метод toString() исполняется, несмотря на то что у объекта o нет метода toString()! Проверить это мы можем с помощью функции Object.getOwnPropertyNames().

> Object.getOwnPropertyNames(o)
< (2) ["name", "surname", "foo"]

Действительно, только три свойства: name, surname и foo. Откуда же вязался метод toString()?

Про сертификаты:  UFM 001 - Проведение поверки - Главный форум метрологов

Особенности javascript

Уязвимость prototype pollution присуща исключительно языку JavaScript. Стало быть, прежде чем разбираться с самой уязвимостью, нам необходимо разобраться в особенностях JavaScript, которые к ней приводят.

Переходим к классам

Прототипная реализация ООП интересна и имеет свои преимущества, но она не так популярна, как классовая. Поэтому когда в ECMAScript 2021 появилось ключевое слово class, разработчики были очень этому рады.

JS стал походить на классический привычный объектно-ориентированный язык. Однако не будем забывать, что классы в нем – всего лишь синтаксический сахар над теми самыми прототипами. На вид – классы, под капотом – по-прежнему прототипы.

Вот так выглядит обычное создание класса в JavaScript:

class Animals {
    constructor(name, specie) {
        this.name = name;
        this.specie = specie;
    }
    sing() {
        return `${this.name} can sing`;
    }
    dance() {
        return `${this.name} can dance`;
    }
}
let bingo = new Animals("Bingo", "Hairy");
console.log(bingo);

Посмотрим в консоль:

Ничего не напоминает?

Все то же свойство __proto__, которое ссылается на Animals.prototype (-> Object.prototype).

Мы видим, что собственные значения свойств (name и specie) определяются внутри метода constructor. Кроме него создаются дополнительные функции sing и dance – методы прототипа.

Подкапотную реализацию этой конструкции мы разбирали только что – это функция-конструктор ключевое слово new.

function Animals(name, specie) {
    this.name = name;
    this.specie = specie;
}
Animals.prototype.sing = function(){
    return `${this.name} can sing`;
}
Animals.prototype.dance = function() {
    return `${this.name} can dance`;
}
let Bingo = new Animals("Bingo", "Hairy");

Последствия прототипирования

После внедрения прототипирования у нас изменился процесс разработки ПО в целом. Как видно из жизненного цикла, прототип пронизывает весь процесс разработки. Прототип стал элементом, который все видят и который все обсуждают: от пользователя до программиста.

Он стал своего рода объединяющим, центральным звеном. Он вывел коммуникацию как с заказчиком, так и внутри компании на новый уровень. Значительно уменьшилась потеря информации по пути от заказчика к программисту, потому что все видят один и тот же прототип.

Если раньше процесс передачи информации выглядел примерно так:

то сейчас он представляет собой что-то вроде этого:

Картинки позаимствовал из презентации Геннадия Драгуна, за что ему премного благодарен.

Прототипирование на постсоветском пространстве


Теперь краткая информация о распространении и тенденциях прототипирования в России и странах СНГ. Как показывает

, проведённый

на Хабре, в половине компаний процесс прототипирования вообще отсутствует.

Однако радует осознание того, что ситуация с прототипированием в компании неудовлетворительна. Это видно по результатам другого опроса: более 70% опрошенных не удовлетворены текущей ситуацией и почти половина из них находится на данный момент в поиске хорошей методики и инструмента. Хорошая тенденция.

Прототипировать ли?

В качестве итогов – плюсы и минусы от внедрения процесса прототипирования.

Прототипное наследование

В программировании мы часто хотим взять что-то и расширить.

Например, у нас есть объект user со своими свойствами и методами, и мы хотим создать объекты admin и guest как его слегка изменённые варианты. Мы хотели бы повторно использовать то, что есть у объекта user, не копировать/переопределять его методы, а просто создать новый объект на его основе.

Прототипное наследование — это возможность языка, которая помогает в этом.

В JavaScript объекты имеют специальное скрытое свойство [[Prototype]] (так оно названо в спецификации), которое либо равно null, либо ссылается на другой объект. Этот объект называется «прототип»:

Прототип даёт нам немного «магии». Когда мы хотим прочитать свойство из object, а оно отсутствует, JavaScript автоматически берёт его из прототипа. В программировании такой механизм называется «прототипным наследованием». Многие интересные возможности языка и техники программирования основываются на нём.

Свойство [[Prototype]] является внутренним и скрытым, но есть много способов задать его.

Одним из них является использование __proto__, например так:

Если мы ищем свойство в rabbit, а оно отсутствует, JavaScript автоматически берёт его из animal.

Например:

Здесь строка (*) устанавливает animal как прототип для rabbit.

Затем, когда alert пытается прочитать свойство rabbit.eats(**), его нет в rabbit, поэтому JavaScript следует по ссылке [[Prototype]] и находит его в animal (смотрите снизу вверх):

Здесь мы можем сказать, что “animal является прототипом rabbit” или “rabbit прототипно наследует от animal“.

Так что если у animal много полезных свойств и методов, то они автоматически становятся доступными у rabbit. Такие свойства называются «унаследованными».

Если у нас есть метод в animal, он может быть вызван на rabbit:

Метод автоматически берётся из прототипа:

Цепочка прототипов может быть длиннее:

Есть только два ограничения:

  1. Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__ по кругу.
  2. Значение __proto__ может быть объектом или null. Другие типы игнорируются.

Это вполне очевидно, но всё же: может быть только один [[Prototype]]. Объект не может наследоваться от двух других объектов.

Прототип используется только для чтения свойств.

Операции записи/удаления работают напрямую с объектом.

В приведённом ниже примере мы присваиваем rabbit собственный метод walk:

Теперь вызов rabbit.walk() находит метод непосредственно в объекте и выполняет его, не используя прототип:

Свойства-аксессоры – исключение, так как запись в него обрабатывается функцией-сеттером. То есть, это, фактически, вызов функции.

По этой причине admin.fullName работает корректно в приведённом ниже коде:

Здесь в строке (*) свойство admin.fullName имеет геттер в прототипе user, поэтому вызывается он. В строке (**) свойство также имеет сеттер в прототипе, который и будет вызван.

В приведённом выше примере может возникнуть интересный вопрос: каково значение this внутри set fullName(value)? Куда записаны свойства this.name и this.surname: в user или в admin?

Ответ прост: прототипы никак не влияют на this.

Неважно, где находится метод: в объекте или его прототипе. При вызове метода this — всегда объект перед точкой.

Таким образом, вызов сеттера admin.fullName= в качестве this использует admin, а не user.

Это на самом деле очень важная деталь, потому что у нас может быть большой объект со множеством методов, от которого можно наследовать. Затем наследующие объекты могут вызывать его методы, но они будут изменять своё состояние, а не состояние объекта-родителя.

Например, здесь animal представляет собой «хранилище методов», и rabbit использует его.

Вызов rabbit.sleep() устанавливает this.isSleeping для объекта rabbit:

Картинка с результатом:

Если бы у нас были другие объекты, такие как bird, snake и т.д., унаследованные от animal, они также получили бы доступ к методам animal. Но this при вызове каждого метода будет соответствовать объекту (перед точкой), на котором происходит вызов, а не animal. Поэтому, когда мы записываем данные в this, они сохраняются в этих объектах.

В результате методы являются общими, а состояние объекта — нет.

Цикл for..in проходит не только по собственным, но и по унаследованным свойствам объекта.

Например:

Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода obj.hasOwnProperty(key): он возвращает true, если у obj есть собственное, не унаследованное, свойство с именем key.

Пример такой фильтрации:

Про сертификаты:  Дроссель клапан сертификат соответствия

В этом примере цепочка наследования выглядит так: rabbit наследует от animal, который наследует от Object.prototype (так как animal – литеральный объект {...}, то это по умолчанию), а затем null на самом верху:

Заметим ещё одну деталь. Откуда взялся метод rabbit.hasOwnProperty? Мы его явно не определяли. Если посмотреть на цепочку прототипов, то видно, что он берётся из Object.prototype.hasOwnProperty. То есть, он унаследован.

…Но почему hasOwnProperty не появляется в цикле for..in в отличие от eats и jumps? Он ведь перечисляет все унаследованные свойства.

Ответ простой: оно не перечислимо. То есть, у него внутренний флаг enumerable стоит false, как и у других свойств Object.prototype. Поэтому оно и не появляется в цикле.

  • В JavaScript все объекты имеют скрытое свойство [[Prototype]], которое является либо другим объектом, либо null.
  • Мы можем использовать obj.__proto__ для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены).
  • Объект, на который ссылается [[Prototype]], называется «прототипом».
  • Если мы хотим прочитать свойство obj или вызвать метод, которого не существует у obj, тогда JavaScript попытается найти его в прототипе.
  • Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное свойство, а не сеттер).
  • Если мы вызываем obj.method(), а метод при этом взят из прототипа, то this всё равно ссылается на obj. Таким образом, методы всегда работают с текущим объектом, даже если они наследуются.
  • Цикл for..in перебирает как свои, так и унаследованные свойства. Остальные методы получения ключей/значений работают только с собственными свойствами объекта.

Прототипы бывают разные…

Существует множество мнений о том, что нужно/можно считать прототипом и какими характеристиками он должен обладать. Чтобы не выставлять своё субъективное видение за истину, я опять обращусь к своду знаний SWEBOK. Он говорит, что прототипом могут считаться как “бумажные” модели, так и пилотные подсистемы, реализуемые как самостоятельные (в терминах управления ресурсами) проекты или бета-версии продуктов.

Прототипы можно разделить на 2 большие группы в зависимости от способов их создания и последующего использования:

image

Одноразовые прототипы являются макетом интерфейса, который в последствии не станет частью готовой системы и на определённом этапе будет «выброшен». Такие прототипы создаются и изменяются быстро, поскольку не требуют качественной реализации. Зачастую они создаются в специализированных инструментах без программирования.

Эволюционный прототип – это предварительная реализация программы, альфа-версия, которая по мере своего развития становится всё ближе и ближе к готовому продукту и, в конце концов, становится им. Эволюционные прототипы менее гибкие, их создание и изменение более длительное и дорогое.

Поскольку на начальном этапе не все требования известны и утверждены, прототип в ходе своего развития может обрасти «заплатками». При таком подходе есть большой риск получить на выходе продукт неудовлетворительного качества. Преимуществом эволюционных прототипов считается то, что, во-первых, уже на ранних стадиях заказчик получает работающую систему, во-вторых, не нужно тратить ресурсы на создание прототипа, который потом будет «выброшен».

У каждого из подходов есть свои преимущества и недостатки. Каждый сам для себя решает, какие прототипы ему создавать в зависимости от решаемой задачи, от особенности процесса разработки ПО в компании, от квалификации сотрудников. Мы для себя решили использовать одноразовое прототипирование, как наиболее гибкий, эффективный и менее рискованный инструмент. На нём я и акцентирую внимание.

Свойство __proto__

Каждый объект в JavaScript имеет свойство __proto__, в котором хранится ссылка на другой объект, являющийся его прототипом.

Через это свойство, этот объект и получает доступ к свойствам и методам прототипа.

По умолчанию у всех объектов в свойство __proto__ записана ссылка на Object.prototype. Однако мы можем изменить это – иначе говоря «унаследовать» объект от другого прототипа.

Ссылки

Примеры:

Другое:

Статический анализ

Данный тип уязвимостей трудно искать простым grep-ом, но можно весьма успешно искать с помощью CodeQL. Существующие CodeQL запросы действительно находят prototype pollution в реальных пакетах, хотя на данный момент покрыты далеко не все варианты этой уязвимости.

Субклассирование

Субклассирование – это наследование и расширение родительского класса дочерним.

Предположим, вам нужно создать класс Cats. Вы могли бы написать его с нуля, но зачем, если уже есть класс Animals, который реализует часть нужного вам функционала. Вы можете просто унаследовать Cats от Animals и добавить то, чего не хватает (например, цвет усов).

Наследование в класс-ориентированном синтаксисе выглядит так:

class Animals {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sing() {
        return `${this.name} can sing`;
    }
    dance() {
        return `${this.name} can dance`;
    }
} 

class Cats extends Animals {
    constructor(name, age, whiskerColor) {
        super(name, age);
        this.whiskerColor = whiskerColor;
    }
    whiskers() {
        return `I have ${this.whiskerColor} whiskers`;
    }
}
let clara = new Cats("Clara", 33, "indigo");

Объект класса Catsclara может использовать свойства и методы как класса Cats, так и класса Animals.

console.log(clara.sing()); // "Clara can sing"
console.log(clara.whiskers()); // "I have indigo whiskers"


Вот так clara выглядит в консоли:

В свойстве clara.__proto__.constructor лежит класс Cats, через него осуществляется доступ к методу whiskers(). Дальше в цепочке прототипов – класс Animals, с методами sing() и dance(). name и age – это свойства самого объекта.

Перепишем этот код в прототипном стиле с использованием метода Object.create():

function Animals(name, age) {
    let newAnimal = Object.create(animalConstructor);
    newAnimal.name = name;
    newAnimal.age = age;
    return newAnimal;
}
let animalConstructor = {
    sing: function() {
        return `${this.name} can sing`;
    },
    dance: function() {
        return `${this.name} can dance`;
    }
}
function Cats(name, age, whiskerColor) {
    let newCat = Animals(name, age);
    Object.setPrototypeOf(newCat, catConstructor);
    newCat.whiskerColor = whiskerColor;
    return newCat;
}
let catConstructor = {
    whiskers() {
        return `I have ${this.whiskerColor} whiskers`;
    }
}
Object.setPrototypeOf(catConstructor, animalConstructor);
const clara = Cats("Clara", 33, "purple");
clara.sing(); // "Clara can sing"
clara.whiskers(); // "I have purple whiskers"

Метод Object.setPrototypeOf принимает два аргумента (объект, которому нужно установить новый прототип, и собственно сам желаемый прототип).

Функция Animals возвращает объект, прототипом которого является animalConstructor. Функция Cats создает объект с помощью конструктора Animals, но принудительно меняет его прототип на catConstructor, добавляя таким образом новые свойства. catConstructor в свою очередь тоже получает прототипом animalConstructor, чтобы образовалась цепочка прототипного наследования.

Таким образом, обычные животные будут иметь доступ только к методам animalConstructor, а кошки – еще и к catConstructor.

***

JavaScript оказался достаточно гибок, чтобы превратить свое прототипное ООП в классовое для удобства разработчиков.

Функции – тоже объекты

Если вас смутило наличие свойств у функции DogObject, доступных через точку (prototype), то вспомните, что в JS функции – это тоже объекты.

В качестве прототипа они имеют объект Function.prototype, который в свою очередь наследует от Object.prototype.

В Function.prototype содержится много дополнительных свойств и методов, которые наследует от него любая функция (call, apply, isPrototypeOf).

Цифры

Трудно подбивать все проекты под одну шкалу: все они очень индивидуальные, сильно различаются по длительности и стоимости, а также по раскладке работ при разработке. Поэтому приведённые ниже цифры носят оценочный и усреднённый характер:

В наших проектах работа с требованиями и проектирование занимают около 30% всей длительности проекта, реализация – около 40%, тестирование – около 30%. Исходя из этого, можно вычислить, что общее время проекта сократилось на величину от 2 до 8%.

Это только сухие цифры, но, как я уже говорил, прототипирование даёт далеко не только уменьшение сроков.

Черный список полей

Чаще всего разработчики просто добавляют __proto__ черный список и не копируют это поле. Так делают даже опытные разработчики (например, кейс npm/ini).

Такой фикс легко обойти используя constructor.prototype вместо __proto__.

С одной стороны этот метод прост в реализации и зачастую его хватает для исправления уязвимости, с другой стороны он не искореняет проблему, потому что все еще остается возможность изменения Object.prototype и других прототипов.

Итоги

JavaScript prototype pollution крайне опасная уязвимость, ее необходимо больше изучать как с точки зрения поиска новых векторов, так и с точки зрения поиска новых гаджетов (эксплуатации). На клиенте совсем не развит вектор, когда пейлод сохранен на сервере, поэтому здесь есть простор для дальнейшего исследования.

Кроме того, у JavaScript есть много других интересных особенностей, которые можно использовать для новых уязвимостей, например DEF CON Safe Mode — Feng Xiao — Discovering Hidden Properties to Attack Node js Ecosystem. Наверняка есть и другие тонкости JavaScript, которые могут приводить к настолько же серьезным или более серьезным последствиям для безопасности приложений.

Вывод

Для нас процесс прототипирования стал однозначно выгодным и полезным.

Если вы ещё не прототипируете – попробуйте, не пожалеете.

Промежуточные итоги. выжимка из теории


Итак, на основе анализа источников можно выделить следующие варианты использования прототипов:

Но мы используем прототипы ещё более широко – добавлю к перечисленным ещё 4 наших варианта.

Оцените статью
Мой сертификат
Добавить комментарий