Same-Origin vs Cross-Origin
- Same-origin: Cùng protocol, domain, port → Truy cập trực tiếp
parent.document, parent.someFunction()
- Cross-origin: Khác domain/port/protocol → **Chỉ dùng
postMessage** (Security policy)
postMessage API
// Gửi message
targetWindow.postMessage(message, targetOrigin);
// Nhận message
window.addEventListener('message', (event) => {
// KHÔNG BAO GIỜ tin cậy event.data mà không verify origin
if (event.origin !== 'https://trusted-domain.com') return;
// Xử lý message
console.log('Received:', event.data);
});
Parent Page (Receiver)
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Parent Page</title>
<style>
body { font-family: sans-serif; padding: 2rem; }
iframe { width: 100%; height: 300px; border: 1px solid #ddd; border-radius: 8px; }
.log { background: #f8f9fa; padding: 1rem; margin-top: 1rem; border-radius: 4px; font-family: monospace; max-height: 200px; overflow-y: auto; }
.log-entry { padding: 0.25rem 0; border-bottom: 1px solid #eee; }
.log-entry:last-child { border-bottom: none; }
.sent { color: #27ae60; }
.received { color: #3498db; }
.error { color: #e74c3c; }
</style>
</head>
<body>
<h1>Parent Page (Receiver)</h1>
<p>Domain: <strong id="parentDomain"></strong></p>
<iframe id="childFrame" src="child.html"></iframe>
<div class="log" id="log"></div>
<script>
document.getElementById('parentDomain').textContent = window.location.origin;
const log = document.getElementById('log');
function addLog(message, type = 'received') {
const div = document.createElement('div');
div.className = `log-entry ${type}`;
div.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
// Danh sách origin được tin cậy
const ALLOWED_ORIGINS = [
'https://localhost:3000',
'https://127.0.0.1:5500',
'https://your-child-domain.com'
];
window.addEventListener('message', (event) => {
// 1. VALIDATE ORIGIN - QUAN TRỌNG NHẤT
if (!ALLOWED_ORIGINS.includes(event.origin)) {
addLog(`BLOCKED: Origin không được tin cậy: ${event.origin}`, 'error');
return;
}
// 2. Xử lý message
const data = event.data;
addLog(`Nhận từ ${event.origin}: ${JSON.stringify(data)}`);
// 3. Reply nếu cần (two-way communication)
if (data.type === 'ping') {
event.source.postMessage({ type: 'pong', timestamp: Date.now() }, event.origin);
}
});
// Gửi message đến iframe (nếu cần)
function sendToChild(data) {
const iframe = document.getElementById('childFrame');
if (iframe.contentWindow) {
iframe.contentWindow.postMessage(data, '*'); // Hoặc origin cụ thể
addLog(`Gửi đến child: ${JSON.stringify(data)}`, 'sent');
}
}
</script>
</body>
</html>
Child Page (Sender) - iframe content
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Child Page (Iframe)</title>
<style>
body { font-family: sans-serif; padding: 1.5rem; background: #fdfdfd; }
button { margin: 0.5rem; padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
.log { background: #fff; border: 1px solid #ddd; padding: 1rem; margin-top: 1rem; border-radius: 4px; font-family: monospace; max-height: 200px; overflow-y: auto; }
</style>
</head>
<body>
<h2>Child Page (Sender)</h2>
<p>Domain: <strong id="childDomain"></strong></p>
<button onclick="sendMessage('hello')>Gửi 'hello'</button>
<button onclick="sendMessage({type: 'userAction', action: 'click', data: {id: 123}})">Gửi object phức tạp</button>
<button onclick="sendPing()">Gửi Ping (chờ Pong)</button>
<div class="log" id="log"></div>
<script>
document.getElementById('childDomain').textContent = window.location.origin;
const log = document.getElementById('log');
const parentOrigin = 'https://localhost:3000'; // Parent origin - CẦN CẤU HÌNH
function addLog(message, type = 'sent') {
const div = document.createElement('div');
div.className = `log-entry ${type}`;
div.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function sendMessage(data) {
// Gửi đến parent
window.parent.postMessage(data, parentOrigin);
addLog(`Đã gửi: ${JSON.stringify(data)}`);
}
function sendPing() {
const message = { type: 'ping', timestamp: Date.now(), id: Math.random() };
window.parent.postMessage(message, parentOrigin);
addLog(`Ping sent: ${message.id}`);
}
// Nhận reply từ parent
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin !== parentOrigin) {
addLog(`BLOCKED reply from: ${event.origin}`, 'error');
return;
}
const data = event.data;
addLog(`Nhận reply: ${JSON.stringify(data)}`, 'received');
});
</script>
</body>
</html>
Pattern nâng cao: Message Channel (Two-way, bảo mật hơn)
// Parent
const channel = new MessageChannel();
iframe.contentWindow.postMessage({ type: 'init' }, childOrigin, [channel.port2]);
channel.port1.onmessage = (event) => {
console.log('Received from child:', event.data);
};
// Child
window.addEventListener('message', (event) => {
if (event.data.type === 'init' && event.ports[0]) {
const port = event.ports[0];
port.onmessage = (e) => {
// Handle message from parent
port.postMessage({ response: 'ok' });
};
// Gửi message đến parent qua port
port.postMessage({ ready: true });
}
});
Security Checklist
- ✅ **Luôn validate
event.origin** trước khi xử lý
- ✅ Gửi
targetOrigin cụ thể (không dùng * trừ khi broadcast)
- ✅ Không gửi dữ liệu nhạy cảm (token, password) qua postMessage
- ✅ Sử dụng
MessageChannel cho two-way communication bảo mật hơn
- ✅ Xác thực message structure (type, payload) trước khi xử lý
- ✅ Rate limiting nếu cần (tránh spam message)
Debug Tips
// Log tất cả message để debug
window.addEventListener('message', (e) => {
console.group(`📨 Message from ${e.origin}`);
console.log('Data:', e.data);
console.log('Source:', e.source);
console.log('Ports:', e.ports);
console.groupEnd();
});