Перейти к содержимому

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 строгим равенством.