CORS-мисконфиги и prototype pollution
Две частые ошибки фронтенд/бэкенд-стыка: слишком открытый CORS и загрязнение прототипа в JavaScript — и как закрыть обе.
CORS управляет тем, какие сторонние сайты могут читать ответы вашего API из браузера. Prototype pollution — уязвимость JS, при которой атакующий через специальные ключи дописывает свойства в общий прототип объектов.
Этот урок объединяет два независимых, но одинаково «тихих» класса проблем. Оба часто появляются из стремления «сделать удобнее» (разрешить запросы отовсюду; рекурсивно слить присланный объект с настройками) и оба закрываются дисциплиной валидации. Относятся к категориям A05: Security Misconfiguration (CORS) и A03/A08 (загрязнение прототипа через недоверенный ввод). Разбираем для защиты собственных приложений.
Часть 1. CORS-мисконфигурации
Зачем это знать защитнику
Браузер по умолчанию запрещает скрипту с сайта A читать ответ запроса к сайту B (Same-Origin Policy). CORS — это контролируемое ослабление этого запрета через заголовки ответа. Ошибка в настройке открывает чужим сайтам доступ к данным ваших аутентифицированных пользователей. Понимая правила, вы выдаёте доступ ровно тем источникам, которым он действительно нужен.
Как возникает уязвимость
Опасный паттерн — «отражать» присланный заголовок Origin обратно и при этом разрешать передачу учётных данных:
# УЯЗВИМО (псевдо-настройка ответа сервера):
Access-Control-Allow-Origin: <любой Origin, который пришёл в запросе>
Access-Control-Allow-Credentials: true
Такая комбинация фактически говорит браузеру: «разреши любому сайту читать ответ и присылать cookie пользователя». Тогда вредоносная страница, открытая жертвой, может от её имени обращаться к вашему API и читать ответ. Отдельная грубая ошибка — Access-Control-Allow-Origin: * вместе с попыткой передавать учётные данные (спецификация это запрещает, но небрежные обёртки иногда обходят запрет «вручную»).
Как защититься
Разрешайте конкретный allow-list доверенных источников, а не «отражайте» любой Origin. Учётные данные включайте только для проверенных источников. И не открывайте CORS шире, чем нужно.
# БЕЗОПАСНО (принцип): источник проверяется по белому списку
allowed = { "https://app.example.com", "https://admin.example.com" }
ЕСЛИ Origin запроса принадлежит allowed:
Access-Control-Allow-Origin: <этот конкретный Origin>
Access-Control-Allow-Credentials: true
ИНАЧЕ:
заголовки CORS не добавлять (браузер сам заблокирует чтение ответа)
Дополнительно: не используйте * для эндпоинтов с приватными данными, перечисляйте только нужные методы и заголовки в Access-Control-Allow-Methods/-Headers, и помните, что CORS защищает чтение ответа, а не выполнение запроса — для изменяющих операций нужна ещё и анти-CSRF защита.
Часть 2. Prototype pollution
Зачем это знать защитнику
В JavaScript почти каждый объект наследует свойства от общего «прародителя» — прототипа. Если код позволяет пользователю задать свойство с именем __proto__ (или через constructor.prototype), атакующий может дописать поле в общий прототип — и оно «появится» у всех объектов программы. Это ведёт к подмене логики, обходу проверок, иногда к более тяжёлым последствиям. Понимая механику, вы не позволяете недоверенным ключам управлять структурой объектов.
Как возникает уязвимость
Классический источник — наивное рекурсивное слияние присланного объекта (merge/extend) без фильтрации опасных ключей:
// УЯЗВИМО: рекурсивный merge без защиты от __proto__
function unsafeMerge(target, source) {
for (const key in source) {
if (typeof source[key] === "object" && source[key] !== null) {
unsafeMerge(target[key] = target[key] || {}, source[key]);
} else {
target[key] = source[key]; // если key === "__proto__", пишем в ПРОТОТИП
}
}
return target;
}
// Демонстрация принципа: ключ "__proto__" в данных дотягивается до общего прототипа
const evilData = JSON.parse('{"__proto__": {"polluted": "yes"}}');
const cfg = unsafeMerge({}, evilData);
const freshObject = {};
console.log(freshObject.polluted); // "yes" — свойство протекло во ВСЕ объекты
Вывод:
yes
Свежесозданный пустой объект внезапно «знает» свойство polluted — потому что оно было записано в общий прототип. Так появляются поля, влияющие на проверки во всём приложении.
Как защититься
Защита — комбинация приёмов. Первое: фильтруйте опасные ключи (__proto__, constructor, prototype) при слиянии или отклоняйте такой ввод. Второе: для словарей-«сумок данных» создавайте объект без прототипа через Object.create(null) — тогда писать в прототип просто некуда. Третье: замораживайте критичные прототипы и валидируйте входные данные по схеме.
// БЕЗОПАСНО: пропускаем опасные ключи + храним данные в объекте без прототипа
function safeMerge(target, source) {
for (const key in source) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue; // не даём управлять прототипом
}
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = safeMerge(target[key] || Object.create(null), source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
const evilData = JSON.parse('{"__proto__": {"polluted": "yes"}}');
const cfg = safeMerge(Object.create(null), evilData);
const freshObject = {};
console.log(freshObject.polluted); // undefined — прототип не тронут
Вывод:
undefined
Опасные ключи отброшены, данные лежат в объекте без прототипа — загрязнять нечего. На уровне проекта также помогают проверенные библиотеки merge/clone (которые уже учитывают эту защиту) и валидация по схеме, отбрасывающая неожиданные поля.
Итоги
- CORS — это контролируемое ослабление Same-Origin Policy; нельзя «отражать» любой Origin вместе с
Allow-Credentials: true. - Разрешайте доступ по allow-list конкретных источников, не используйте
*для приватных данных, помните про отдельную анти-CSRF защиту. - Prototype pollution — запись в общий прототип через ключи
__proto__/constructor/prototypeпри наивном слиянии ввода. - Защита: фильтровать опасные ключи, хранить данные в
Object.create(null), замораживать критичные прототипы, валидировать по схеме. - Все проверки — только на своих приложениях; несанкционированный доступ к чужим системам наказуем (УК РФ ст. 272).