Same-Origin Policy & postMessage
Same-Origin Policy (SOP) — базовая изоляция браузера: скрипт с origin A не может читать ответы от origin B без явного CORS-разрешения; postMessage — безопасный канал кросс-origin коммуникации между фреймами; слабая проверка origin в postMessage — регулярная уязвимость в bug bounty программах.
// Same-Origin Policy: origin = scheme + host + port (все три должны совпадать)//// https://example.com:443 vs http://example.com:80 -> разные (scheme + port)// https://app.example.com vs https://api.example.com -> разные (host)// https://example.com vs https://example.com:443 -> одинаковые (порт 443 implicit)
// Что БЛОКИРУЕТ SOP:// ✗ чтение ответа cross-origin fetch/XHR без CORS// ✗ доступ к iframe.contentDocument / contentWindow другого origin// ✗ чтение localStorage / cookies / DOM другого origin
// Что НЕ блокирует SOP (важно для понимания атак!):// ✓ отправка simple POST cross-origin (HTML form) — именно это использует CSRF// ✓ загрузка <img>, <script>, <link rel=stylesheet> с других origin// ✓ навигация (location.href = 'https://other.com')// ✓ WebSocket-соединение (CORS не применяется к WS — нужна серверная проверка Origin)
// document.domain — legacy способ ослабить SOP (deprecated, не использовать):// Оба субдомена устанавливают document.domain = 'example.com' → один origin// Удалён из Firefox 101+, планируется удаление в Chrome// postMessage: безопасная кросс-origin коммуникация
// Отправитель (parent window)const iframe = document.querySelector('iframe');iframe.contentWindow.postMessage( { type: 'CONFIG', theme: 'dark', userId: 42 }, 'https://widget.example.com' // targetOrigin — ОБЯЗАТЕЛЬНО указать! // '*' → любой origin получит сообщение — ОПАСНО для sensitive data);
// Получатель (код внутри iframe)window.addEventListener('message', e => { // КРИТИЧЕСКИ важно: всегда проверять origin! if (e.origin !== 'https://app.example.com') return; // отбрасываем чужих
// e.source — ссылка на window-отправитель (для ответа) // e.data — переданные данные (structured clone) // e.ports — MessagePort[] для Channel Messaging
if (e.data?.type === 'CONFIG') { applyConfig(e.data); e.source.postMessage({ type: 'ACK', received: true }, e.origin); // ответ }});// Безопасный SDK-виджет в iframe (реальный паттерн)class WidgetBridge { #allowedOrigin; constructor(allowedOrigin) { this.#allowedOrigin = allowedOrigin; window.addEventListener('message', this.#onMessage.bind(this)); } #onMessage({ origin, data, source }) { // Строгое равенство — substring-атака обойдёт includes/indexOf! if (origin !== this.#allowedOrigin) return; this.#dispatch(data); } send(msg) { window.parent.postMessage(msg, this.#allowedOrigin); }}// УЯЗВИМОСТЬ — слабые проверки origin:// if (e.origin.includes('example.com')) ... → evil-example.com обойдёт!// if (e.origin.startsWith('https://')) ... → любой HTTPS-origin пройдёт!// Только: e.origin === 'https://app.example.com'
// MessageChannel: структурированный двусторонний каналconst { port1, port2 } = new MessageChannel();iframe.contentWindow.postMessage('init', 'https://widget.example.com', [port2]);port1.onmessage = ({ data }) => console.log('from iframe:', data);port1.postMessage({ cmd: 'render', items: [1, 2, 3] });
// opener: window.open() → открытое окно имеет доступ к window.opener// <a href="..." target="_blank" rel="noopener noreferrer"> — всегда!Итог: SOP изолирует origins по умолчанию; postMessage — безопасный канал при строгой проверке origin строгим равенством.