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