HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2
НОВЫЕ ТОРГОВАЯ НОВОСТИ ЧАТ
loading...
Скрыть
Вернуться   ANTICHAT > БЕЗОПАСНОСТЬ И УЯЗВИМОСТИ > Уязвимости
   
Ответ
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 06.04.2026, 03:53
Luxkerr
Постоянный
Регистрация: 14.11.2023
Сообщений: 524
С нами: 1315891

Репутация: 0


По умолчанию



Prototype Pollution редко выглядит как громкая уязвимость. Обычно всё начинается с куска кода, который никто не считает опасным: функция слияния объектов, разбор параметров в структуру настроек, универсальный helper для deep copy, пустой config-объект, который потом уходит в рендер, загрузчик скрипта, шаблонизатор или системный вызов. До первого инцидента это воспринимается как нормальная инженерная обвязка. Проблема в том, что ошибка живёт не в одном конкретном месте. Один фрагмент принимает данные, другой пишет по ключу, третий позже читает унаследованное свойство как штатный параметр. В итоге баг возникает не на уровне одной функции, а на стыке нескольких безобидных решений.

Этим Prototype Pollution и неприятна. По отдельности JSON.parse(), Object.assign(), разбор location.hash, рендер компонента или вызов child_process могут выглядеть совершенно спокойно. Но если пользовательское значение однажды доехало до прототипа, приложение начинает работать с чужими свойствами как со своими. На клиенте это выливается в DOM XSS, подмену src, srcdoc, обработчиков и конфигурации виджетов. На сервере - в подмену параметров фреймворка, шаблонизатора, промежуточного обработчика или системного API. Именно поэтому Prototype Pollution плохо ловится интуицией: источник находится в одном слое, эффект проявляется в другом, а между ними часто лежит обычный служебный код, который годами не вызывал подозрений.

Эта уязвимость редко живёт как один баг в одном месте. Обычно это цепочка из трёх частей:
  • источник управляемых данных;
  • путь записи в прототип;
  • участок кода, который позже использует загрязнённое свойство.



Где ломается модель объектов
Почему пустой объект не пустой
У обычного объекта в JavaScript есть собственные поля и прототип. Когда код читает свойство, движок сначала ищет его в самом объекте, потом поднимается по цепочке прототипов. Поэтому пустой {} на деле не совсем пустой - он наследует поведение от Object.prototype.

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

Если загрязнён Object.prototype, эффект расходится по большому куску приложения сразу. Если меняется прототип конкретного объекта настроек, масштаб меньше, но для эксплуатации этого часто уже хватает. Уязвимости не обязательно нужно испортить весь рантайм. Иногда достаточно сломать один объект, который потом уйдёт в чувствительный код.

proto, prototype и constructor.prototype
На этой теме часто спотыкаются даже те, кто уже сталкивался с Prototype Pollution.

Код:
__proto__
- исторический доступ к прототипу объекта.
prototype - свойство функции-конструктора, из которого создаются прототипы экземпляров.
constructor.prototype - обходной путь к тому же объекту.



Откуда берётся Prototype Pollution
Небезопасное рекурсивное слияние
Самый частый источник - самописное глубокое слияние объектов. Код выглядит настолько обычным, что его почти не замечают на ревью.

JavaScript:


Код:
function
isPlainObject
(
value
)
{
return
value
!==
null
&&
typeof
value
===
"object"
&&
!
Array
.
isArray
(
value
)
;
}
function
deepMerge
(
target, source
)
{
for
(
const
key
of
Object
.
keys
(
source
)
)
{
const
src
=
source
[
key
]
;
const
dst
=
target
[
key
]
;
if
(
isPlainObject
(
src
)
&&
isPlainObject
(
dst
)
)
{
deepMerge
(
dst
,
src
)
;
}
else
{
target
[
key
]
=
src
;
}
}
return
target
;
}
На нормальных данных это просто служебная функция. Проблема начинается, когда входной объект содержит специальный путь к прототипу.

JavaScript:


Код:
const
payload
=
JSON
.
parse
(
'{"__proto__":{"visible":true}}'
)
;
const
state
=
{
}
;
deepMerge
(
state
,
payload
)
;
console
.
log
(
state
.
visible
)
;
// true
console
.
log
(
(
{
}
)
.
visible
)
;
// true
console
.
log
(
{
test
:
1
}
.
visible
)
;
// true
Здесь полезно остановиться на механике. Сам JSON.parse() ещё не делает ничего “магического”. Он всего лишь создаёт объект, у которого есть собственное поле proto. Опасность появляется позже, когда этот объект подхватывает код, который рекурсивно пишет по ключам и тем самым сворачивает обычную строку proto в переход к прототипу.

Это и есть одна из причин, почему баг живёт так долго. Разработчик смотрит на JSON.parse() - там всё спокойно. Смотрит на deepMerge() - тоже вроде без криминала. Проблема рождается на стыке.

Параметры URL и разбор пути
Во фронтенде источник pollution часто сидит в коде, который вообще не воспринимается как опасный. Например, страница умеет превращать query string во вложенный объект настроек.

JavaScript:


Код:
function
setByPath
(
target, path, value
)
{
const
parts
=
path
.
split
(
"."
)
;
let
cursor
=
target
;
for
(
let
i
=
0
;
i
"}}'
)
;
deepMerge
(
{
}
,
payload
)
;
renderCard
(
{
}
)
;
Ключевая проблема тут не в innerHTML как таковом. Она в том, что код читает необязательное свойство у объекта настроек, не проверяя, откуда это свойство пришло - из самого объекта или из прототипа.

На ревью такие места проще узнавать по трём признакам сразу:
  • создаётся пустой объект настроек;
  • часть полей не инициализируется явно;
  • чтение идёт как обычное if (options.html), без проверки принадлежности самому объекту.
Цитата:

В статье: "XSS-атака без секретов: от простого alert до захвата сессии" мы подробнее разобрали тему DOM XSS: какие бывают XSS-цепочки, чем они отличаются и какие защитные меры реально работают.

Подмена script.src
Во фронтенде опасны не только HTML-вставки. Нередко загрязнение доезжает до загрузки скриптов.

JavaScript:


Код:
function
loadWidget
(
options = {}
)
{
const
script
=
document
.
createElement
(
"script"
)
;
script
.
src
=
options
.
src
||
"/static/widget.js"
;
document
.
head
.
appendChild
(
script
)
;
}
Такой фрагмент часто выглядит даже аккуратно: есть значение по умолчанию, есть отдельная функция загрузки. Но если options.src подхватится из прототипа, код поведёт себя так, будто адрес скрипта задан штатным образом.

JavaScript:


Код:
const
payload
=
JSON
.
parse
(
'{"__proto__":{"src":"https://example.invalid/widget.js"}}'
)
;
deepMerge
(
{
}
,
payload
)
;
loadWidget
(
{
}
)
;
Это полезный пример не только для охоты за XSS. Тут хорошо показано, почему Prototype Pollution опасна для фронтенда с большим количеством сторонних библиотек, аналитики, рекламных скриптов и виджетов. Там конфигурационные объекты встречаются везде, а многие поля остаются необязательными.

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

Можно так:

JavaScript:


Код:
function
setupTimer
(
options = {}
)
{
if
(
options
.
delayHandler
)
{
setTimeout
(
options
.
delayHandler
,
100
)
;
}
}
Или так:

JavaScript:


Код:
function
bindAction
(
button, options = {}
)
{
if
(
options
.
onClick
)
{
button
.
setAttribute
(
"onclick"
,
options
.
onClick
)
;
}
}
Чем больше в приложении самописных виджетов, обвязок над сторонними библиотеками и “гибких” конфигурационных слоёв, тем больше шансов найти именно такие гаджеты, а не учебный innerHTML.

Как это выглядит в коде SPA
Если свести типовой клиентский сценарий к минимуму, получится что-то такое:

JavaScript:


Код:
function
parseHash
(
)
{
const
result
=
{
}
;
const
raw
=
location
.
hash
.
slice
(
1
)
;
for
(
const
pair
of
raw
.
split
(
"&"
)
)
{
const
[
key
,
value
]
=
pair
.
split
(
"="
)
;
if
(
key
)
{
setByPath
(
result
,
decodeURIComponent
(
key
)
,
decodeURIComponent
(
value
||
""
)
)
;
}
}
return
result
;
}
function
initWidget
(
)
{
const
userOptions
=
parseHash
(
)
;
const
defaults
=
{
theme
:
"light"
}
;
deepMerge
(
defaults
,
userOptions
)
;
renderCard
(
{
}
)
;
}
Нам интересна не сама функция parseHash(), а общий рисунок:
  • данные пришли из URL;
  • разложились по путям;
  • слились в объект настроек;
  • другой участок кода прочитал “пустой” объект, унаследовавший загрязнённое поле.
Именно так Prototype Pollution и живёт в реальных интерфейсах. Не в одном эффектном фрагменте, а в нескольких обычных функциях, каждая из которых по отдельности выглядит безобидно.

Server-side: где начинается опасная часть
На сервере Prototype Pollution неприятнее по двум причинам. Во-первых, загрязнение живёт в процессе до перезапуска. Во-вторых, даже аккуратная проверка может случайно превратиться в отказ в обслуживании, если задеть не тот путь и не тот гаджет.

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

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

JavaScript:


Код:
const
express
=
require
(
"express"
)
;
const
app
=
express
(
)
;
app
.
use
(
express
.
json
(
)
)
;
function
isPlainObject
(
value
)
{
return
value
!==
null
&&
typeof
value
===
"object"
&&
!
Array
.
isArray
(
value
)
;
}
function
deepMerge
(
target, source
)
{
for
(
const
key
of
Object
.
keys
(
source
)
)
{
const
src
=
source
[
key
]
;
const
dst
=
target
[
key
]
;
if
(
isPlainObject
(
src
)
&&
isPlainObject
(
dst
)
)
{
deepMerge
(
dst
,
src
)
;
}
else
{
target
[
key
]
=
src
;
}
}
return
target
;
}
app
.
post
(
"/api/profile"
,
(
req, res
)
=>
{
const
profile
=
{
}
;
deepMerge
(
profile
,
req
.
body
)
;
res
.
json
(
{
saved
:
true
}
)
;
}
)
;
app
.
get
(
"/api/ping"
,
(
req, res
)
=>
{
const
options
=
{
}
;
res
.
status
(
options
.
status
||
200
)
.
json
(
{
ok
:
true
}
)
;
}
)
;
app
.
listen
(
3000
)
;
Если POST /api/profile доедет до прототипа и положит туда status, следующий запрос к GET /api/ping внезапно начнёт отвечать уже не тем кодом, который ждали в приложении.

Это очень полезный практический кусок по двум причинам. Во-первых, он показывает server-side pollution без опасной операционки. Во-вторых, сразу видно главное отличие от клиентской части: загрязнение не исчезает после одного рендера. Оно остаётся жить в процессе и начинает влиять на последующие запросы.

Почему child_process становится гаджетом
Теперь можно переходить к серверным эффектам посерьёзнее.

Опасность child_process не в том, что разработчик обязательно запускает внешнюю команду с пользовательской строкой. Для Prototype Pollution это даже не нужно. Проблема начинается там, где код передаёт в API запуска процесса пустой или неполный объект опций и рассчитывает, что недостающие поля просто останутся не заданными.

JavaScript:


Код:
const
{
execFile
}
=
require
(
"node:child_process"
)
;
function
convertImage
(
inputPath, outputPath, options = {}
)
{
return
execFile
(
"convert"
,
[
inputPath
,
outputPath
]
,
options
)
;
}
Снаружи это выглядит как чистая функция-обёртка. Но если options получит чувствительные поля из прототипа, поведение вызова может измениться. Именно поэтому в server-side разборе Prototype Pollution так много внимания уделяют не строкам команд, а конфигурационным объектам вокруг них.

Шаблонизаторы и скрытая конфигурация
С шаблонизаторами история похожая. Проблема обычно не в том, что приложению напрямую подсовывают произвольный шаблон. Проблема в том, что в render(), compile() или renderFile() передаётся объект настроек, часть которого должна была быть пустой или безопасной по умолчанию.

JavaScript:


Код:
function
renderPage
(
engine, template, data, options = {}
)
{
return
engine
.
render
(
template
,
data
,
options
)
;
}
Если раньше в процессе уже произошло загрязнение прототипа, options может внезапно получить поля, которые влияют на работу шаблонизатора - поиск файлов, поведение рендера, режимы компиляции, экранирование или другие параметры.

Особенно больно это находить в больших Node.js-приложениях, где шаблонизатор обёрнут несколькими внутренними слоями, а источник pollution сидит вообще в другой части сервиса.

Цитата:

Если хочется подробнее разобраться с темой безопасности SPA, загляните в нашу статью: "Тестирование безопасности SPA: уязвимости React, Vue, Angular"

Инструменты, которые реально помогают
Для клиентской части хорошо работают инструменты, которые умеют быстро находить источник pollution и проверять, доезжает ли значение до опасного места в DOM. Здесь особенно полезны DOM Invader и похожие средства для анализа клиентских цепочек. Для первичного скрининга встречаются и более узкие утилиты вроде ppmap.

Для серверной части автоматизация полезна, но не стоит ждать от неё чуда. Наружный скан может подсветить безопасные индикаторы - странный код ответа, поведение JSON, неожиданные заголовки. Настоящая ценность обычно появляется при разборе зависимостей, обвязок над child_process, шаблонизаторов и функций слияния данных.

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

Как чинить это без косметики



Не делать “универсальное” глубокое слияние пользовательских объектов
Первое и самое полезное исправление - перестать бездумно вливать произвольный пользовательский объект в структуру приложения. Вместо “умного” общего deepMerge() почти всегда лучше работает явное извлечение разрешённых полей.

JavaScript:


Код:
function
normalizeProfile
(
input
)
{
return
{
displayName
:
typeof
input
.
displayName
===
"string"
?
input
.
displayName
:
""
,
theme
:
input
.
theme
===
"dark"
?
"dark"
:
"light"
,
pageSize
:
Number
.
isInteger
(
input
.
pageSize
)
?
input
.
pageSize
:
20
}
;
}
Такой код менее “изящный”, чем универсальный merge helper, зато здесь нет магии. Разрешённые поля видны сразу, остальные просто не попадут дальше.

Не использовать обычные объекты как словари
Если нужна структура “ключ-значение” с пользовательскими ключами, лучше брать Map или объект без прототипа.

JavaScript:


Код:
const
safeDict
=
Object
.
create
(
null
)
;
safeDict
.
userTheme
=
"dark"
;
Или так:

JavaScript:


Код:
const
cache
=
new
Map
(
)
;
cache
.
set
(
"user:42"
,
{
theme
:
"dark"
}
)
;
Обычный объект слишком легко превращается из удобного контейнера в поверхность атаки.

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

JavaScript:


Код:
function
renderCard
(
options = {}
)
{
const
container
=
document
.
getElementById
(
"card"
)
;
if
(
Object
.
hasOwn
(
options
,
"html"
)
)
{
container
.
innerHTML
=
options
.
html
;
return
;
}
container
.
textContent
=
"Default content"
;
}
Это не универсальная защита от всех проблем вокруг Prototype Pollution, но очень полезный барьер в местах, где код использует конфигурационные объекты.

Почему заморозка прототипа не поможет
Идея с Object.freeze(Object.prototype) регулярно всплывает как быстрое средство защиты. Как дополнительный барьер она полезна, но не решает архитектурную проблему сама по себе. У неё есть цена: совместимость, порядок инициализации, поведение полифилов, неожиданные побочные эффекты в старом коде.

Если приложение уже массово опирается на универсальные merge helper’ы, пустые объекты настроек и динамическую запись по путям, заморозка прототипа будет скорее аварийным тормозом, чем нормальным исправлением.

Вместо заключения
Как только в приложении есть путь записи в прототип и участок кода, который читает несуществующее свойство как допустимое значение по умолчанию, дальше уже начинается не “теоретическая особенность JavaScript”, а вполне рабочая поверхность атаки. На клиенте она уходит в DOM XSS и подмену загрузки. На сервере - в управление поведением процесса, фреймворка и системных вызовов.

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





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.