fix(个人主页): 回归纯sandbox隔离,移除注入脚本和父页面路由

- 移除沙箱拦截脚本注入、handleOpenUrl、opc:page:open-url postMessage
- 仅靠浏览器sandbox强制隔离:无allow-popups/allow-top-navigation
- 内部所有跳转仅限iframe内,window.open和target=_top被浏览器阻止
- 彻底消除父页面navigate导致的白屏
- srcDoc直接渲染用户HTML,不注入任何脚本
This commit is contained in:
Pine
2026-09-04 23:37:52 +08:00
parent 3cee99362e
commit 06a29f9b0f
+10 -78
View File
@@ -3,7 +3,7 @@
// 内容区 iframe 渲染用户自写 HTML(沙箱)或嵌入第三方网页,右上角保留
// 「更多选项 + 返回」标准胶囊按钮,底部展示能力接入说明或数据核实提醒。
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { isOsPath } from "../../../utils/navigationMode";
import { Avatar, Button, Dropdown, Empty, Input, Modal, Segmented, Spin, Switch, Tag, message } from "antd";
import type { MenuProps } from "antd";
@@ -17,20 +17,6 @@ import styles from "./UserHome.module.less";
/** 本页窗口的固定 routeId(窗口 id 即 routeId,用于返回/退出时关闭)。 */
const HOME_ROUTE_ID = "core.user-home";
/**
* 沙箱拦截脚本:注入到用户自写 HTML 顶部,捕获所有"试图跳出 iframe"的动作。
* - window.open() → 不打开新标签,改为 postMessage 通知父页面
* - <a target="_blank|_top|_parent"> → 阻止默认跳转,改为 postMessage
* - form target=_blank|_top|_parent → 阻止提交
* 父页面收到 opc:page:open-url 后统一路由:内部用户主页开新 usershome,外部链接开系统浏览器。
*/
const SANDBOX_INTERCEPT_SCRIPT = `<script>(function(){try{var _o=window.open;window.open=function(u){if(u){try{window.parent.postMessage({__opcBridge:!0,type:"opc:page:open-url",url:String(u)},"*")}catch(e){}}return null;};document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");if(a&&a.href){var t=(a.target||"").toLowerCase();if(t==="_blank"||t==="_top"||t==="_parent"||t==="_external"){e.preventDefault();try{window.parent.postMessage({__opcBridge:!0,type:"opc:page:open-url",url:a.href},"*")}catch(e2){}}}},!0);document.addEventListener("submit",function(e){var f=e.target;if(f&&f.target){var t=f.target.toLowerCase();if(t==="_blank"||t==="_top"||t==="_parent"){e.preventDefault();}}},!0);}catch(e){}})();<\/script>`;
/** 给用户 HTML 注入沙箱拦截脚本(前置,优先于用户自身脚本执行)。 */
function injectSandboxGuard(html: string): string {
return SANDBOX_INTERCEPT_SCRIPT + (html || "");
}
/** 可接入的平台能力预设(开发者可声明自己主页接入了哪些平台数据)。 */
const CAP_PRESETS: { key: string; name: string; desc: string }[] = [
{ key: "orders", name: "订单数据", desc: "我的服务订单记录" },
@@ -59,8 +45,6 @@ export default function UserHomePage() {
return "";
}, [location.pathname, pendingTarget]);
const close = useOsWindows((s) => s.close);
const openApp = useOsRoute((s) => s.openApp);
const navigate = useNavigate();
const startChat = useStartChat();
const [data, setData] = useState<UserPageRender | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -121,68 +105,15 @@ export default function UserHomePage() {
);
}, [uid]);
/**
* 统一处理沙箱内部请求"打开新页面":
* - 内部用户主页(/usershome/xxx)→ 新开 usershome 页面(OS 窗口 / 浏览器导航)
* - 外部 http(s) 链接 → 新标签页/系统浏览器打开
* - 相对路径 → **在 iframe 内部导航**,绝不影响父页面
* 绝不让沙箱内部直接影响父页面或开不受控的新窗口。
*/
const handleOpenUrl = useCallback((rawUrl: string) => {
if (!rawUrl) return;
const url = rawUrl.trim();
// 1. 内部用户主页:匹配 /usershome/<uid>(支持完整 URL 或相对路径)
const usershomeMatch = url.match(/\/usershome\/([^/?#]+)/);
if (usershomeMatch) {
const targetUid = decodeURIComponent(usershomeMatch[1]);
if (targetUid) {
if (isOsPath(window.location.pathname)) {
openApp(HOME_ROUTE_ID, `/usershome/${encodeURIComponent(targetUid)}`);
} else {
navigate(`/usershome/${encodeURIComponent(targetUid)}`);
}
return;
}
}
// 2. 外部 http/https 链接 → 新标签页打开(noopener 防反向控制)
if (/^https?:\/\//i.test(url)) {
window.open(url, "_blank", "noopener,noreferrer");
return;
}
// 3. 相对路径 / 站内路径 → 在 iframe 内部导航,父页面不受影响
// (用户 HTML 内部的 /order?xxx、/profile 等路由只在沙箱内生效)
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../") || url.startsWith("?")) {
const frame = iframeRef.current;
if (frame?.contentWindow) {
try {
frame.contentWindow.location.href = url;
} catch {
// 跨源时无法直接设置 location,改用 src
frame.src = url;
}
}
return;
}
// 4. 其他协议(mailto:/tel: 等)→ 直接打开
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {
window.open(url, "_blank", "noopener,noreferrer");
}
}, [openApp, navigate]);
// 页面脚本可主动请求身份数据,或请求打开新页面(沙箱拦截后转发)。
// 页面脚本可主动请求身份数据(postMessage {__opcBridge:true,type:"opc:page:get-me"})。
// 沙箱本身已强制隔离(无 allow-popups / allow-top-navigation),内部所有跳转仅限 iframe 内。
useEffect(() => {
const onMsg = (e: MessageEvent) => {
if (!e.data?.__opcBridge) return;
if (e.data.type === "opc:page:get-me") pushIdentity();
else if (e.data.type === "opc:page:open-url") handleOpenUrl(e.data.url);
if (e.data?.__opcBridge && e.data.type === "opc:page:get-me") pushIdentity();
};
window.addEventListener("message", onMsg);
return () => window.removeEventListener("message", onMsg);
}, [pushIdentity, handleOpenUrl]);
}, [pushIdentity]);
const handleRefresh = useCallback(() => {
void load();
@@ -267,9 +198,10 @@ export default function UserHomePage() {
const page = data.page;
const def = data.default;
const isHtml = page?.pageType === "html";
// 安全沙箱(严禁 allow-popups / allow-top-navigation):
// - 内部 window.open / target=_blank 被注入脚本拦截,经 postMessage 交父页面统一路由
// - 内部普通链接在 iframe 内导航,不影响外部
// 安全沙箱(纯浏览器强制隔离,不含 allow-popups / allow-top-navigation):
// - 内部所有跳转(window.location、<a href>、form 提交)仅限 iframe 内,无法影响父页面
// - window.open / target=_blank 被浏览器阻止(无 allow-popups
// - target=_top / _parent / window.top.location 被浏览器阻止(无 allow-top-navigation
// - html 模式不含 allow-same-origin,脚本运行在独特源
// - embed 模式第三方站需 allow-same-origin 才能用 localStorage/cookie
const sandbox = isHtml
@@ -321,7 +253,7 @@ export default function UserHomePage() {
sandbox={sandbox}
referrerPolicy="no-referrer"
src={isHtml ? undefined : page.embedUrl || undefined}
srcDoc={isHtml ? injectSandboxGuard(page.htmlContent || "") : undefined}
srcDoc={isHtml ? page.htmlContent || undefined : undefined}
onLoad={pushIdentity}
/>
</div>