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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user