fix(iframe): validate trusted parent messages
This commit is contained in:
@@ -5,9 +5,10 @@ import { useEffect, useCallback, type ReactNode } from "react";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { setPlayerBearerToken } from "@/lib/lottery-auth";
|
||||
import {
|
||||
isIframeOriginAllowed,
|
||||
loadIframeAllowedOrigins,
|
||||
messageToken,
|
||||
resolvePostMessageTargetOrigin,
|
||||
resolveTrustedParentMessage,
|
||||
} from "@/lib/iframe-origins";
|
||||
|
||||
function sanitizeUrlForParent(href: string): string {
|
||||
@@ -123,39 +124,38 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
|
||||
console.log("[IframeBridge] Setting up iframe communication");
|
||||
|
||||
const handleMessage = async (event: MessageEvent): Promise<void> => {
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
await loadIframeAllowedOrigins(true);
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
console.warn("[IframeBridge] Rejected message from:", event.origin);
|
||||
return;
|
||||
}
|
||||
const data = await resolveTrustedParentMessage(event);
|
||||
if (data === null) {
|
||||
console.warn("[IframeBridge] Rejected untrusted parent message from:", event.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = event;
|
||||
if (!data || typeof data !== "object") return;
|
||||
|
||||
console.log("[IframeBridge] Received message:", data.type);
|
||||
|
||||
switch (data.type) {
|
||||
// 主站发送初始化 Token
|
||||
case "MAIN_INIT_TOKEN":
|
||||
if (data.token) {
|
||||
case "MAIN_INIT_TOKEN": {
|
||||
const token = messageToken(data);
|
||||
if (token !== null) {
|
||||
console.log("[IframeBridge] Received initial token");
|
||||
setBearerToken(data.token);
|
||||
setPlayerBearerToken(data.token);
|
||||
setBearerToken(token);
|
||||
setPlayerBearerToken(token);
|
||||
// 勿再 notifyReady(),否则主站会重复 MAIN_INIT_TOKEN 导致消息刷屏
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 主站刷新 Token
|
||||
case "MAIN_REFRESH_TOKEN":
|
||||
if (data.token) {
|
||||
case "MAIN_REFRESH_TOKEN": {
|
||||
const token = messageToken(data);
|
||||
if (token !== null) {
|
||||
console.log("[IframeBridge] Received refreshed token");
|
||||
setBearerToken(data.token);
|
||||
setPlayerBearerToken(data.token);
|
||||
setBearerToken(token);
|
||||
setPlayerBearerToken(token);
|
||||
notifyTokenRefreshed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 主站通知 Token 即将过期
|
||||
case "MAIN_TOKEN_EXPIRING":
|
||||
@@ -173,7 +173,7 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode {
|
||||
|
||||
// 主站导航请求
|
||||
case "MAIN_NAVIGATE":
|
||||
if (data.path && typeof data.path === "string") {
|
||||
if (typeof data.path === "string") {
|
||||
const nextPath = resolveSafeInAppPath(data.path);
|
||||
if (nextPath) {
|
||||
window.history.pushState({}, "", nextPath);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { parseJwtExp } from "@/lib/jwt-payload";
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { useErrorStore } from "@/stores/error-store";
|
||||
import {
|
||||
isIframeOriginAllowed,
|
||||
loadIframeAllowedOrigins,
|
||||
messageToken,
|
||||
resolvePostMessageTargetOrigin,
|
||||
resolveTrustedParentMessage,
|
||||
} from "@/lib/iframe-origins";
|
||||
|
||||
/** Token 过期前警告阈值(毫秒) */
|
||||
@@ -118,25 +119,21 @@ export function useTokenRefresh(): {
|
||||
void loadIframeAllowedOrigins();
|
||||
|
||||
const handleMessage = async (event: MessageEvent): Promise<void> => {
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
await loadIframeAllowedOrigins(true);
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
console.warn("[TokenRefresh] Ignored message from unknown origin:", event.origin);
|
||||
return;
|
||||
}
|
||||
const data = await resolveTrustedParentMessage(event);
|
||||
if (data === null) {
|
||||
console.warn("[TokenRefresh] Ignored untrusted parent message from:", event.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = event;
|
||||
if (!data || typeof data !== "object") return;
|
||||
|
||||
// 处理主站发送的新 Token(兼容 MAIN_REFRESH_TOKEN 与 LOTTERY_TOKEN_REFRESH_RESPONSE)
|
||||
const token = messageToken(data);
|
||||
if (
|
||||
(data.type === "LOTTERY_TOKEN_REFRESH_RESPONSE" || data.type === "MAIN_REFRESH_TOKEN") &&
|
||||
data.token
|
||||
token !== null
|
||||
) {
|
||||
console.log("[TokenRefresh] Received new token from parent");
|
||||
pendingRefreshRef.current = 0;
|
||||
setBearerToken(data.token);
|
||||
setBearerToken(token);
|
||||
retryCountRef.current = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,42 @@ export function isIframeOriginAllowed(origin: string): boolean {
|
||||
return allowedOrigins.includes(normalized);
|
||||
}
|
||||
|
||||
function isPlainMessageObject(value: unknown): value is Record<string, unknown> {
|
||||
if (Object.prototype.toString.call(value) !== "[object Object]") return false;
|
||||
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
/** 仅接收当前 iframe 的真实父窗口发来的白名单消息。 */
|
||||
export async function resolveTrustedParentMessage(
|
||||
event: MessageEvent,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
if (typeof window === "undefined" || window.parent === window) {
|
||||
return null;
|
||||
}
|
||||
if (event.source !== window.parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
await loadIframeAllowedOrigins(true);
|
||||
if (!isIframeOriginAllowed(event.origin)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return isPlainMessageObject(event.data) ? event.data : null;
|
||||
}
|
||||
|
||||
export function messageToken(data: Record<string, unknown>): string | null {
|
||||
const token = data.token;
|
||||
if (typeof token !== "string") return null;
|
||||
|
||||
const trimmed = token.trim();
|
||||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
/** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin;无白名单时拒绝发送 */
|
||||
export function resolvePostMessageTargetOrigin(): string | null {
|
||||
const allowedOrigins = getKnownIframeAllowedOrigins();
|
||||
|
||||
Reference in New Issue
Block a user