Дізнайтеся про безпечну кросориджинну комунікацію за допомогою PostMessage API. Вивчіть його можливості, ризики безпеки та найкращі практики для усунення вразливостей.
Кросориджинна комунікація: патерни безпеки з PostMessage API
У сучасному вебі додаткам часто потрібно взаємодіяти з ресурсами з різних джерел. Політика однакового походження (Same-Origin Policy, SOP) є ключовим механізмом безпеки, який обмежує доступ скриптів до ресурсів з іншого джерела. Однак існують легітимні сценарії, коли кросориджинна комунікація необхідна. API postMessage надає контрольований механізм для цього, але важливо розуміти його потенційні ризики безпеки та впроваджувати відповідні патерни безпеки.
Розуміння політики однакового походження (SOP)
Політика однакового походження є фундаментальною концепцією безпеки у веб-браузерах. Вона обмежує веб-сторінки у виконанні запитів до домену, відмінного від того, з якого було завантажено сторінку. Джерело (origin) визначається схемою (протоколом), хостом (доменом) і портом. Якщо хоча б один із цих параметрів відрізняється, джерела вважаються різними. Наприклад:
https://example.comhttps://www.example.comhttp://example.comhttps://example.com:8080
Все це різні джерела, і SOP обмежує прямий доступ скриптів між ними.
Знайомство з PostMessage API
API postMessage надає безпечний та контрольований механізм для кросориджинної комунікації. Він дозволяє скриптам надсилати повідомлення іншим вікнам (наприклад, iframe, новим вікнам або вкладкам) незалежно від їхнього джерела. Вікно-одержувач може прослуховувати ці повідомлення та обробляти їх відповідним чином.
Базовий синтаксис для надсилання повідомлення:
otherWindow.postMessage(message, targetOrigin);
otherWindow: посилання на цільове вікно (наприклад,window.parent,iframe.contentWindowабо об'єкт вікна, отриманий зwindow.open).message: дані, які ви хочете надіслати. Це може бути будь-який об'єкт JavaScript, який можна серіалізувати (наприклад, рядки, числа, об'єкти, масиви).targetOrigin: вказує джерело, до якого ви хочете надіслати повідомлення. Це надзвичайно важливий параметр безпеки.
На стороні одержувача потрібно прослуховувати подію message:
window.addEventListener('message', function(event) {
// ...
});
Об'єкт event містить наступні властивості:
event.data: повідомлення, надіслане іншим вікном.event.origin: джерело вікна, яке надіслало повідомлення.event.source: посилання на вікно, яке надіслало повідомлення.
Ризики безпеки та вразливості
Хоча postMessage пропонує спосіб обійти обмеження SOP, він також створює потенційні ризики безпеки, якщо не реалізований обережно. Ось деякі поширені вразливості:
1. Невідповідність цільового джерела (targetOrigin)
Неможливість перевірити властивість event.origin є критичною вразливістю. Якщо одержувач сліпо довіряє повідомленню, будь-який веб-сайт може надіслати шкідливі дані. Завжди перевіряйте, чи event.origin відповідає очікуваному джерелу, перш ніж обробляти повідомлення.
Приклад (вразливий код):
window.addEventListener('message', function(event) {
// НЕ РОБІТЬ ЦЬОГО!
processMessage(event.data);
});
Приклад (безпечний код):
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Отримано повідомлення з недовіреного джерела:', event.origin);
return;
}
processMessage(event.data);
});
2. Впровадження даних (Data Injection)
Обробка отриманих даних (event.data) як виконуваного коду або їх пряме впровадження в DOM може призвести до вразливостей міжсайтового скриптингу (XSS). Завжди очищуйте та перевіряйте отримані дані перед їх використанням.
Приклад (вразливий код):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
document.body.innerHTML = event.data; // НЕ РОБІТЬ ЦЬОГО!
}
});
Приклад (безпечний код):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
const sanitizedData = sanitize(event.data); // Реалізуйте належну функцію очищення
document.getElementById('message-container').textContent = sanitizedData;
}
});
function sanitize(data) {
// Реалізуйте тут надійну логіку очищення.
// Наприклад, використовуйте DOMPurify або подібну бібліотеку
return DOMPurify.sanitize(data);
}
3. Атаки «людина посередині» (MITM)
Якщо комунікація відбувається через незахищений канал (HTTP), зловмисник MITM може перехопити та змінити повідомлення. Завжди використовуйте HTTPS для безпечного зв'язку.
4. Підробка міжсайтових запитів (CSRF)
Якщо одержувач виконує дії на основі отриманого повідомлення без належної перевірки, зловмисник може підробити повідомлення, щоб змусити одержувача виконати небажані дії. Впроваджуйте механізми захисту від CSRF, такі як включення секретного токена в повідомлення та його перевірка на стороні одержувача.
5. Використання символу-джокера (*) в targetOrigin
Встановлення targetOrigin у значення * дозволяє будь-якому джерелу отримувати повідомлення. Цього слід уникати, якщо це не є абсолютно необхідним, оскільки це нівелює мету безпеки на основі джерела. Якщо ви все ж таки повинні використовувати *, переконайтеся, що ви впровадили інші сильні заходи безпеки, такі як коди автентифікації повідомлень (MAC).
Приклад (уникайте цього):
otherWindow.postMessage(message, '*'); // Уникайте використання '*', якщо це не є абсолютно необхідним
Патерни безпеки та найкращі практики
Щоб зменшити ризики, пов'язані з postMessage, дотримуйтесь цих патернів безпеки та найкращих практик:
1. Сувора перевірка джерела
Завжди перевіряйте властивість event.origin на стороні одержувача. Порівнюйте її з попередньо визначеним списком довірених джерел. Використовуйте сувору рівність (===) для порівняння.
2. Очищення та перевірка даних
Очищуйте та перевіряйте всі дані, отримані через postMessage, перед їх використанням. Використовуйте відповідні методи очищення залежно від того, як будуть використовуватися дані (наприклад, екранування HTML, кодування URL, перевірка вводу). Використовуйте бібліотеки, такі як DOMPurify, для очищення HTML.
3. Коди автентифікації повідомлень (MAC)
Додавайте код автентифікації повідомлення (MAC) до повідомлення, щоб забезпечити його цілісність та автентичність. Відправник обчислює MAC за допомогою спільного секретного ключа і включає його в повідомлення. Одержувач перераховує MAC за допомогою того ж самого спільного секретного ключа і порівнює його з отриманим MAC. Якщо вони збігаються, повідомлення вважається автентичним і незміненим.
Приклад (використання HMAC-SHA256):
// Відправник
async function sendMessage(message, targetOrigin, sharedSecret) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: message,
signature: signatureHex
};
otherWindow.postMessage(securedMessage, targetOrigin);
}
// Одержувач
async function receiveMessage(event, sharedSecret) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Отримано повідомлення з недовіреного джерела:', event.origin);
return;
}
const securedMessage = event.data;
const message = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Повідомлення автентичне!');
processMessage(message); // Продовжити обробку повідомлення
} else {
console.error('Перевірка підпису повідомлення не вдалася!');
}
}
Важливо: Спільний секретний ключ повинен бути безпечно згенерований і збережений. Уникайте жорсткого кодування ключа в коді.
4. Використання Nonce та міток часу
Щоб запобігти атакам повторного відтворення (replay attacks), додавайте до повідомлення унікальний nonce (одноразовий номер) та мітку часу. Одержувач може перевірити, що nonce не використовувався раніше, а мітка часу знаходиться в межах допустимого діапазону. Це зменшує ризик того, що зловмисник повторно відправить перехоплені раніше повідомлення.
5. Принцип найменших привілеїв
Надавайте іншому вікну лише мінімально необхідні привілеї. Наприклад, якщо іншому вікну потрібно лише читати дані, не дозволяйте йому записувати дані. Проектуйте свій протокол комунікації з урахуванням принципу найменших привілеїв.
6. Політика безпеки контенту (CSP)
Використовуйте Політику безпеки контенту (CSP), щоб обмежити джерела, з яких можна завантажувати скрипти, та дії, які ці скрипти можуть виконувати. Це може допомогти зменшити наслідки вразливостей XSS, які можуть виникнути через неправильну обробку даних postMessage.
7. Перевірка вхідних даних
Завжди перевіряйте структуру та формат отриманих даних. Визначте чіткий формат повідомлення та переконайтеся, що отримані дані відповідають цьому формату. Це допомагає запобігти неочікуваній поведінці та вразливостям.
8. Безпечна серіалізація даних
Використовуйте безпечний формат серіалізації даних, такий як JSON, для серіалізації та десеріалізації повідомлень. Уникайте використання форматів, які дозволяють виконання коду, таких як eval() або Function().
9. Обмеження розміру повідомлення
Обмежуйте розмір повідомлень, що надсилаються через postMessage. Великі повідомлення можуть споживати надмірні ресурси та потенційно призводити до атак типу «відмова в обслуговуванні» (DoS).
10. Регулярні аудити безпеки
Проводьте регулярні аудити безпеки вашого коду для виявлення та усунення потенційних вразливостей. Приділяйте особливу увагу реалізації postMessage та переконайтеся, що дотримуються всі найкращі практики безпеки.
Приклад сценарію: безпечна комунікація між iframe та його батьківським елементом
Розглянемо сценарій, де iframe, розміщений на https://iframe.example.com, повинен обмінюватися даними зі своєю батьківською сторінкою, розміщеною на https://parent.example.com. Iframe повинен надсилати дані користувача на батьківську сторінку для обробки.
Iframe (https://iframe.example.com):
// Згенеруйте спільний секретний ключ (замініть на безпечний метод генерації ключа)
const sharedSecret = 'ВАШ_БЕЗПЕЧНИЙ_СПІЛЬНИЙ_СЕКРЕТ';
// Отримати дані користувача
const userData = {
name: 'John Doe',
email: 'john.doe@example.com'
};
// Надіслати дані користувача на батьківську сторінку
async function sendUserData(userData) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: userData,
signature: signatureHex
};
parent.postMessage(securedMessage, 'https://parent.example.com');
}
sendUserData(userData);
Батьківська сторінка (https://parent.example.com):
// Спільний секретний ключ (повинен збігатися з ключем iframe)
const sharedSecret = 'ВАШ_БЕЗПЕЧНИЙ_СПІЛЬНИЙ_СЕКРЕТ';
window.addEventListener('message', async function(event) {
if (event.origin !== 'https://iframe.example.com') {
console.warn('Отримано повідомлення з недовіреного джерела:', event.origin);
return;
}
const securedMessage = event.data;
const userData = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Повідомлення автентичне!');
// Обробити дані користувача
console.log('Дані користувача:', userData);
} else {
console.error('Перевірка підпису повідомлення не вдалася!');
}
});
Важливі примітки:
- Замініть
ВАШ_БЕЗПЕЧНИЙ_СПІЛЬНИЙ_СЕКРЕТна безпечно згенерований спільний секретний ключ. - Спільний секретний ключ повинен бути однаковим як в iframe, так і на батьківській сторінці.
- Цей приклад використовує HMAC-SHA256 для автентифікації повідомлень.
Висновок
API postMessage є потужним інструментом для забезпечення кросориджинної комунікації у веб-додатках. Однак важливо розуміти потенційні ризики безпеки та впроваджувати відповідні патерни для їх зменшення. Дотримуючись патернів безпеки та найкращих практик, викладених у цьому посібнику, ви можете безпечно використовувати postMessage для створення надійних та безпечних веб-додатків.
Завжди пам'ятайте про пріоритетність безпеки та будьте в курсі останніх найкращих практик для веб-розробки. Регулярно перевіряйте свій код та конфігурації безпеки, щоб переконатися, що ваші додатки захищені від потенційних вразливостей.