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).
Проверьте себя
1. Почему опасно отражать присланный Origin в Access-Control-Allow-Origin вместе с Allow-Credentials: true?
AЭто замедляет ответ сервера
BЭто разрешает любому стороннему сайту читать ответы вашего API и слать cookie аутентифицированного пользователя
CЭто ломает кодировку UTF-8 в заголовках
DЭто отключает HTTPS
2. Что такое prototype pollution в JavaScript?
AУтечка памяти из-за слишком больших массивов
BЗапись свойства в общий прототип объектов через ключи вроде __proto__, из-за чего поле появляется у всех объектов
CОшибка кодировки JSON
DСлишком частые сетевые запросы к API
3. Какой приём напрямую устраняет почву для prototype pollution в словаре-«сумке данных»?
AСохранять данные в строку JSON
BСоздавать объект без прототипа через Object.create(null), чтобы писать в прототип было некуда
CИспользовать только массивы вместо объектов
DУвеличить размер кучи (heap)