Представь: ты интегрируешь OAuth авторизацию. Пользователь нажимает «Войти через Google» — открывается попап с Google, пользователь даёт разрешения, попап закрывается, и твоя страница получает токен. Как попап передаёт токен обратно? Через postMessage — единственный безопасный канал связи между окнами с разными источниками.
Разные окна браузера изолированы друг от друга по правилам same-origin policy — они не могут читать DOM и переменные друг друга (если домены разные). postMessage — официальный, безопасный способ отправить сообщение между окнами, где важна проверка источника.
message это обычное событие с теми же паттернами обработкиpostMessage это кастомные события на уровне браузерных окон// Синтаксис: window.open(url, target, features)
const popup = window.open(
'https://auth.example.com/oauth',
'oauth-popup', // имя окна (target)
'width=600,height=700,left=100,top=100' // параметры окна
)
// popup — ссылка на открытое окно (объект window)
if (!popup) {
// Браузер заблокировал popup — предупредить пользователя
alert('Разрешите всплывающие окна для этого сайта')
}window.open(url, '_blank') // новая вкладка
window.open(url, '_self') // текущее окно (как переход)
window.open(url, '_parent') // родительский фрейм
window.open(url, '_top') // верхний уровень (выход из фреймов)
window.open(url, 'myWindow') // именованное окноДочернее окно может обратиться к открывшему его окну через window.opener:
// В дочернем окне (oauth-popup):
if (window.opener) {
// Передать результат авторизации обратно
window.opener.postMessage({ type: 'oauth-success', token: 'abc123' }, 'https://myapp.com')
window.close() // закрыть popup
}Доступ к DOM родительского окна через window.opener.document ограничен политикой одного источника (same-origin policy).
postMessage — единственный безопасный способ общения между окнами с разными источниками:
// Отправитель (родительское окно)
const popup = window.open('https://payment.example.com', 'payment')
// Отправить сообщение с проверкой источника
popup.postMessage(
{ type: 'init', orderId: '12345', amount: 1990 },
'https://payment.example.com' // разрешить только этому источнику
)
// Получатель — любое окно слушает входящие сообщения
window.addEventListener('message', (event) => {
// ВАЖНО: всегда проверять источник!
if (event.origin !== 'https://payment.example.com') {
console.warn('Сообщение от недоверенного источника:', event.origin)
return
}
console.log('Получено сообщение:', event.data)
console.log('От кого:', event.origin)
console.log('source:', event.source) // ссылка на window-отправитель
})// Получить доступ к iframe
const frame = document.getElementById('payment-frame')
// Отправить сообщение в iframe
frame.contentWindow.postMessage({ action: 'pay' }, 'https://payment.example.com')
// Слушать ответы от iframe
window.addEventListener('message', (event) => {
if (event.source === frame.contentWindow) {
console.log('Ответ от iframe:', event.data)
}
})// ПЛОХО: принимать сообщения от любого источника
window.addEventListener('message', (event) => {
executeCode(event.data) // ОПАСНО — не проверяем event.origin!
})
// ПЛОХО: отправлять в любой источник
popup.postMessage(sensitiveData, '*') // '*' = кому угодно — опасно
// ХОРОШО: всегда проверять и указывать origin
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED_ORIGIN) return // отклонить чужих
processMessage(event.data)
})
popup.postMessage(data, TRUSTED_ORIGIN) // конкретный origin1. Принимать postMessage без проверки event.origin — уязвимость XSS
// ПЛОХО — любой сайт может отправить сообщение
window.addEventListener('message', (event) => {
processPayment(event.data) // ОПАСНО — не проверяем кто прислал!
})
// ХОРОШО — всегда проверять источник
const TRUSTED_ORIGIN = 'https://payment.example.com'
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED_ORIGIN) {
console.warn('Сообщение от недоверенного источника:', event.origin)
return
}
processPayment(event.data)
})2. Использовать '*' как targetOrigin при postMessage с чувствительными данными
// ПЛОХО — сообщение с токеном отправится любому окну
popup.postMessage({ token: 'secret-token-123' }, '*') // опасно!
// ХОРОШО — указывай конкретный origin
popup.postMessage({ token: 'secret-token-123' }, 'https://myapp.com')
// Если origin не совпадает — сообщение не будет доставлено3. Обращаться к popup.document без проверки same-origin
// ПЛОХО — выбросит SecurityError если popup на другом домене
const popup = window.open('https://other-domain.com')
const title = popup.document.title // SecurityError: cross-origin access!
// ХОРОШО — используй postMessage для коммуникации
popup.postMessage({ type: 'get-title' }, 'https://other-domain.com')
window.addEventListener('message', event => {
if (event.data.type === 'title-response') console.log(event.data.title)
})window.open + postMessagepostMessageСимуляция postMessage между окнами: OAuth popup, обмен сообщениями с проверкой источника
// Симуляция межоконного взаимодействия через mock postMessage систему
// В браузере окна — реальные объекты Window
// --- Mock Window и MessageBus ---
function createMockWindow(origin, name = 'unnamed') {
const listeners = []
let opener = null
const win = {
name,
origin,
opener: null,
closed: false,
// Симуляция window.postMessage
postMessage(data, targetOrigin) {
if (targetOrigin !== '*' && targetOrigin !== this.origin) {
console.log(`[${name}] postMessage отклонён: origin "${this.origin}" не совпадает с targetOrigin "${targetOrigin}"`)
return
}
const event = { data, origin: this.origin, source: this }
console.log(`[${name}] postMessage → данные: ${JSON.stringify(data)}`)
// Рассылаем сообщение всем слушателям (симуляция)
this._deliverMessage(event)
},
addEventListener(type, handler) {
if (type === 'message') listeners.push(handler)
},
// Внутренний метод: принять входящее сообщение
_receiveMessage(data, fromOrigin, fromWindow) {
const event = { data, origin: fromOrigin, source: fromWindow }
listeners.forEach(h => h(event))
},
_deliverMessage(event) {
// В реальности браузер доставляет сообщение в целевое окно
// Здесь это делает MessageBus
},
close() {
this.closed = true
console.log(`[${name}] окно закрыто`)
},
}
return win
}
// MessageBus связывает окна
function createMessageBus() {
const windows = new Map()
return {
register(win) {
windows.set(win.name, win)
},
send(fromWin, toWin, data, targetOrigin) {
if (targetOrigin !== '*' && targetOrigin !== toWin.origin) {
console.log(`[Bus] Блокировка: targetOrigin "${targetOrigin}" не совпадает с "${toWin.origin}"`)
return
}
console.log(`[Bus] ${fromWin.name} → ${toWin.name}: ${JSON.stringify(data)}`)
toWin._receiveMessage(data, fromWin.origin, fromWin)
},
}
}
// --- Демо 1: OAuth popup ---
console.log('=== OAuth Popup симуляция ===')
const mainApp = createMockWindow('https://myapp.com', 'main')
const oauthPopup = createMockWindow('https://auth.provider.com', 'oauth-popup')
oauthPopup.opener = mainApp
const bus = createMessageBus()
bus.register(mainApp)
bus.register(oauthPopup)
// Главное окно слушает результат OAuth
mainApp.addEventListener('message', (event) => {
if (event.origin !== 'https://auth.provider.com') {
console.log('[main] Блокируем сообщение от', event.origin)
return
}
console.log('[main] Получен OAuth результат:', event.data)
if (event.data.type === 'oauth-success') {
console.log(`[main] Авторизован! Токен: ${event.data.token}`)
console.log('[main] Сохраняем токен и обновляем UI...')
}
})
// OAuth popup завершает авторизацию
console.log('[oauth-popup] Пользователь нажал "Разрешить"')
bus.send(oauthPopup, mainApp, { type: 'oauth-success', token: 'eyJhbGciOiJSUzI1NiJ9...' }, 'https://myapp.com')
oauthPopup.close()
// --- Демо 2: Виджет оплаты ---
console.log('\n=== Виджет оплаты (iframe) ===')
const shopPage = createMockWindow('https://shop.example.com', 'shop')
const payWidget = createMockWindow('https://pay.gateway.com', 'payment-widget')
bus.register(shopPage)
bus.register(payWidget)
// Виджет слушает команды от магазина
payWidget.addEventListener('message', (event) => {
if (event.origin !== 'https://shop.example.com') return
console.log('[payment-widget] Получена команда:', event.data)
if (event.data.action === 'initialize') {
console.log(`[payment-widget] Инициализируем платёж на сумму ${event.data.amount} руб.`)
// Симулируем подтверждение
setTimeout(() => {
bus.send(payWidget, shopPage, { type: 'payment-ready' }, 'https://shop.example.com')
}, 100)
}
if (event.data.action === 'charge') {
console.log(`[payment-widget] Обрабатываем оплату...`)
setTimeout(() => {
bus.send(payWidget, shopPage, { type: 'payment-success', transactionId: 'TXN-98765' }, 'https://shop.example.com')
}, 150)
}
})
// Магазин слушает ответы виджета
shopPage.addEventListener('message', (event) => {
if (event.origin !== 'https://pay.gateway.com') return
console.log('[shop] Сообщение от виджета:', event.data)
if (event.data.type === 'payment-ready') {
console.log('[shop] Виджет готов — отправляем команду оплаты')
bus.send(shopPage, payWidget, { action: 'charge', orderId: 'ORD-42' }, 'https://pay.gateway.com')
}
if (event.data.type === 'payment-success') {
console.log(`[shop] Оплата прошла! ID транзакции: ${event.data.transactionId}`)
}
})
// Запускаем сценарий
bus.send(shopPage, payWidget, { action: 'initialize', amount: 4990 }, 'https://pay.gateway.com')
// --- Демо 3: Атака через некорректный origin ---
console.log('\n=== Проверка безопасности (блокировка чужих сообщений) ===')
const maliciousWindow = createMockWindow('https://evil.hacker.com', 'attacker')
bus.register(maliciousWindow)
// Попытка взломщика отправить сообщение
bus.send(maliciousWindow, mainApp, { type: 'oauth-success', token: 'stolen!' }, 'https://myapp.com')
// mainApp проверит event.origin и отклонит — т.к. origin 'https://evil.hacker.com'Представь: ты интегрируешь OAuth авторизацию. Пользователь нажимает «Войти через Google» — открывается попап с Google, пользователь даёт разрешения, попап закрывается, и твоя страница получает токен. Как попап передаёт токен обратно? Через postMessage — единственный безопасный канал связи между окнами с разными источниками.
Разные окна браузера изолированы друг от друга по правилам same-origin policy — они не могут читать DOM и переменные друг друга (если домены разные). postMessage — официальный, безопасный способ отправить сообщение между окнами, где важна проверка источника.
message это обычное событие с теми же паттернами обработкиpostMessage это кастомные события на уровне браузерных окон// Синтаксис: window.open(url, target, features)
const popup = window.open(
'https://auth.example.com/oauth',
'oauth-popup', // имя окна (target)
'width=600,height=700,left=100,top=100' // параметры окна
)
// popup — ссылка на открытое окно (объект window)
if (!popup) {
// Браузер заблокировал popup — предупредить пользователя
alert('Разрешите всплывающие окна для этого сайта')
}window.open(url, '_blank') // новая вкладка
window.open(url, '_self') // текущее окно (как переход)
window.open(url, '_parent') // родительский фрейм
window.open(url, '_top') // верхний уровень (выход из фреймов)
window.open(url, 'myWindow') // именованное окноДочернее окно может обратиться к открывшему его окну через window.opener:
// В дочернем окне (oauth-popup):
if (window.opener) {
// Передать результат авторизации обратно
window.opener.postMessage({ type: 'oauth-success', token: 'abc123' }, 'https://myapp.com')
window.close() // закрыть popup
}Доступ к DOM родительского окна через window.opener.document ограничен политикой одного источника (same-origin policy).
postMessage — единственный безопасный способ общения между окнами с разными источниками:
// Отправитель (родительское окно)
const popup = window.open('https://payment.example.com', 'payment')
// Отправить сообщение с проверкой источника
popup.postMessage(
{ type: 'init', orderId: '12345', amount: 1990 },
'https://payment.example.com' // разрешить только этому источнику
)
// Получатель — любое окно слушает входящие сообщения
window.addEventListener('message', (event) => {
// ВАЖНО: всегда проверять источник!
if (event.origin !== 'https://payment.example.com') {
console.warn('Сообщение от недоверенного источника:', event.origin)
return
}
console.log('Получено сообщение:', event.data)
console.log('От кого:', event.origin)
console.log('source:', event.source) // ссылка на window-отправитель
})// Получить доступ к iframe
const frame = document.getElementById('payment-frame')
// Отправить сообщение в iframe
frame.contentWindow.postMessage({ action: 'pay' }, 'https://payment.example.com')
// Слушать ответы от iframe
window.addEventListener('message', (event) => {
if (event.source === frame.contentWindow) {
console.log('Ответ от iframe:', event.data)
}
})// ПЛОХО: принимать сообщения от любого источника
window.addEventListener('message', (event) => {
executeCode(event.data) // ОПАСНО — не проверяем event.origin!
})
// ПЛОХО: отправлять в любой источник
popup.postMessage(sensitiveData, '*') // '*' = кому угодно — опасно
// ХОРОШО: всегда проверять и указывать origin
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED_ORIGIN) return // отклонить чужих
processMessage(event.data)
})
popup.postMessage(data, TRUSTED_ORIGIN) // конкретный origin1. Принимать postMessage без проверки event.origin — уязвимость XSS
// ПЛОХО — любой сайт может отправить сообщение
window.addEventListener('message', (event) => {
processPayment(event.data) // ОПАСНО — не проверяем кто прислал!
})
// ХОРОШО — всегда проверять источник
const TRUSTED_ORIGIN = 'https://payment.example.com'
window.addEventListener('message', (event) => {
if (event.origin !== TRUSTED_ORIGIN) {
console.warn('Сообщение от недоверенного источника:', event.origin)
return
}
processPayment(event.data)
})2. Использовать '*' как targetOrigin при postMessage с чувствительными данными
// ПЛОХО — сообщение с токеном отправится любому окну
popup.postMessage({ token: 'secret-token-123' }, '*') // опасно!
// ХОРОШО — указывай конкретный origin
popup.postMessage({ token: 'secret-token-123' }, 'https://myapp.com')
// Если origin не совпадает — сообщение не будет доставлено3. Обращаться к popup.document без проверки same-origin
// ПЛОХО — выбросит SecurityError если popup на другом домене
const popup = window.open('https://other-domain.com')
const title = popup.document.title // SecurityError: cross-origin access!
// ХОРОШО — используй postMessage для коммуникации
popup.postMessage({ type: 'get-title' }, 'https://other-domain.com')
window.addEventListener('message', event => {
if (event.data.type === 'title-response') console.log(event.data.title)
})window.open + postMessagepostMessageСимуляция postMessage между окнами: OAuth popup, обмен сообщениями с проверкой источника
// Симуляция межоконного взаимодействия через mock postMessage систему
// В браузере окна — реальные объекты Window
// --- Mock Window и MessageBus ---
function createMockWindow(origin, name = 'unnamed') {
const listeners = []
let opener = null
const win = {
name,
origin,
opener: null,
closed: false,
// Симуляция window.postMessage
postMessage(data, targetOrigin) {
if (targetOrigin !== '*' && targetOrigin !== this.origin) {
console.log(`[${name}] postMessage отклонён: origin "${this.origin}" не совпадает с targetOrigin "${targetOrigin}"`)
return
}
const event = { data, origin: this.origin, source: this }
console.log(`[${name}] postMessage → данные: ${JSON.stringify(data)}`)
// Рассылаем сообщение всем слушателям (симуляция)
this._deliverMessage(event)
},
addEventListener(type, handler) {
if (type === 'message') listeners.push(handler)
},
// Внутренний метод: принять входящее сообщение
_receiveMessage(data, fromOrigin, fromWindow) {
const event = { data, origin: fromOrigin, source: fromWindow }
listeners.forEach(h => h(event))
},
_deliverMessage(event) {
// В реальности браузер доставляет сообщение в целевое окно
// Здесь это делает MessageBus
},
close() {
this.closed = true
console.log(`[${name}] окно закрыто`)
},
}
return win
}
// MessageBus связывает окна
function createMessageBus() {
const windows = new Map()
return {
register(win) {
windows.set(win.name, win)
},
send(fromWin, toWin, data, targetOrigin) {
if (targetOrigin !== '*' && targetOrigin !== toWin.origin) {
console.log(`[Bus] Блокировка: targetOrigin "${targetOrigin}" не совпадает с "${toWin.origin}"`)
return
}
console.log(`[Bus] ${fromWin.name} → ${toWin.name}: ${JSON.stringify(data)}`)
toWin._receiveMessage(data, fromWin.origin, fromWin)
},
}
}
// --- Демо 1: OAuth popup ---
console.log('=== OAuth Popup симуляция ===')
const mainApp = createMockWindow('https://myapp.com', 'main')
const oauthPopup = createMockWindow('https://auth.provider.com', 'oauth-popup')
oauthPopup.opener = mainApp
const bus = createMessageBus()
bus.register(mainApp)
bus.register(oauthPopup)
// Главное окно слушает результат OAuth
mainApp.addEventListener('message', (event) => {
if (event.origin !== 'https://auth.provider.com') {
console.log('[main] Блокируем сообщение от', event.origin)
return
}
console.log('[main] Получен OAuth результат:', event.data)
if (event.data.type === 'oauth-success') {
console.log(`[main] Авторизован! Токен: ${event.data.token}`)
console.log('[main] Сохраняем токен и обновляем UI...')
}
})
// OAuth popup завершает авторизацию
console.log('[oauth-popup] Пользователь нажал "Разрешить"')
bus.send(oauthPopup, mainApp, { type: 'oauth-success', token: 'eyJhbGciOiJSUzI1NiJ9...' }, 'https://myapp.com')
oauthPopup.close()
// --- Демо 2: Виджет оплаты ---
console.log('\n=== Виджет оплаты (iframe) ===')
const shopPage = createMockWindow('https://shop.example.com', 'shop')
const payWidget = createMockWindow('https://pay.gateway.com', 'payment-widget')
bus.register(shopPage)
bus.register(payWidget)
// Виджет слушает команды от магазина
payWidget.addEventListener('message', (event) => {
if (event.origin !== 'https://shop.example.com') return
console.log('[payment-widget] Получена команда:', event.data)
if (event.data.action === 'initialize') {
console.log(`[payment-widget] Инициализируем платёж на сумму ${event.data.amount} руб.`)
// Симулируем подтверждение
setTimeout(() => {
bus.send(payWidget, shopPage, { type: 'payment-ready' }, 'https://shop.example.com')
}, 100)
}
if (event.data.action === 'charge') {
console.log(`[payment-widget] Обрабатываем оплату...`)
setTimeout(() => {
bus.send(payWidget, shopPage, { type: 'payment-success', transactionId: 'TXN-98765' }, 'https://shop.example.com')
}, 150)
}
})
// Магазин слушает ответы виджета
shopPage.addEventListener('message', (event) => {
if (event.origin !== 'https://pay.gateway.com') return
console.log('[shop] Сообщение от виджета:', event.data)
if (event.data.type === 'payment-ready') {
console.log('[shop] Виджет готов — отправляем команду оплаты')
bus.send(shopPage, payWidget, { action: 'charge', orderId: 'ORD-42' }, 'https://pay.gateway.com')
}
if (event.data.type === 'payment-success') {
console.log(`[shop] Оплата прошла! ID транзакции: ${event.data.transactionId}`)
}
})
// Запускаем сценарий
bus.send(shopPage, payWidget, { action: 'initialize', amount: 4990 }, 'https://pay.gateway.com')
// --- Демо 3: Атака через некорректный origin ---
console.log('\n=== Проверка безопасности (блокировка чужих сообщений) ===')
const maliciousWindow = createMockWindow('https://evil.hacker.com', 'attacker')
bus.register(maliciousWindow)
// Попытка взломщика отправить сообщение
bus.send(maliciousWindow, mainApp, { type: 'oauth-success', token: 'stolen!' }, 'https://myapp.com')
// mainApp проверит event.origin и отклонит — т.к. origin 'https://evil.hacker.com'Реализуй класс WindowBridge — систему типобезопасного общения между окнами. Метод connect(localWin, remoteWin, trustedOrigin) создаёт соединение. Метод send(type, data) отправляет сообщение в удалённое окно. Метод on(type, handler) подписывается на сообщения определённого типа от доверенного источника. Метод destroy() удаляет все обработчики.
connect: _messageHandler проверяет event.origin !== this._trustedOrigin, затем берёт тип из event.data.type и вызывает handler из this._handlers. send: вызывает remote._receiveMessage с { type, data }. on: this._handlers.set(type, handler). destroy: removeEventListener и _handlers.clear().