查看: 189|回复: 3

ArkWeb内嵌H5信任边界:Cookie注入时机与失效传播排障清单

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在HarmonyOS混合应用里,原生与H5之间的凭据怎么下发、能力怎么放行、退出后怎么一起失效,其实是同一个信任边界问题的三个侧面。社区里反复冒出的提问——H5请求怎么带上App登录态、token能不能拼在URL后面、原生退出后H5为何还显示已登录、切换账号后预加载的H5为何串号——本质上都在问:原生和H5之间信任到哪里为止,状态由谁做主。本文基于原实现整理出一张风险矩阵、三条设计原则,并重点校准ArkWeb几个容易写错却“跑得通”的API用法,最后收成一份可直接用于评审的清单。

一、先把交互拆成三个命题

任何一次原生与H5的交互,基本可归入三类命题:身份贯通(App登录态如何传给H5)、能力开放(H5如何拉起原生能力)、失效传播(退出或踢下线后如何同步失效)。身份贯通与失效传播其实是同一件事的两端——一个凭据方案好不好,关键看回收能力,而不是传递能力。

二、四个信任域与四条通道

很多人把“原生到H5”想成一根管道,实际中间隔着两次界:
  1. 原生进程 ──①──> Web内核(Cookie库/网络栈) ──②──> 页面脚本 ──③──> 服务端
  2. 可信        半可信(系统管理)        不可信        可信
复制代码

两个常见误判都来自忽略中间那道界:其一,以为“Cookie注入到内核”就安全了,但脚本域能不能读到取决于有没有HttpOnly,与注入方式无关;其二,以为“桥只在我打开的页面里生效”,实际桥对象注册在整个WebView实例上,实例内加载的任何页面都能调到同一个桥对象,导航到站外也一样,除非用permission白名单收敛。

跨域传递凭据只有四条通道:URL、普通Cookie、HttpOnly Cookie、JSBridge。选型时最该看的是“能否撤销”这一列。URL和HttpOnly Cookie的差距不在“能不能被看见”,而在能不能收回;JSBridge常被当成更安全的替代,但它的可撤销性比Cookie更差——返回值一旦进了脚本域,原生就管不着那些副本了。特别提醒:URL拼凭据会把凭据交给所有能看日志、看历史、看Referer的系统,这与攻击者有没有心无关,是设计本身在往外送数据。

三、三条设计原则

第一,凭据默认不下发到脚本域。能只放在内核就不放脚本,能完全不传递最好。

第二,能力按意图授权,不按对象或路由授权。H5的任何入参都不作为可信输入。

第三,状态只有一个源,其余都是副本。副本是必然存在的:Cookie一份、H5的localStorage可能又存一份、内存缓存一份、服务端会话一份。所以“立刻失效”不是删一份就完事,而要三层联动——服务端作废让残留副本即使还在也无效,本地清理让后续请求不再携带,通知页面自清让界面立即对上。这是纵深防御,不是重复劳动。

四、凭据怎么给,更要怎么收

选型可按两个问题走:H5与App后端是否同域(或可控的父子域)?否的话,走请求代理(由原生代发请求并附加鉴权头)或一次性票据;是的话,再看凭据是否允许落到持久化介质,允许就用HttpOnly Cookie,且必须带齐HttpOnly + Secure + 正确的SameSite。

请求代理值得单独说:ArkTS侧用onInterceptRequest事件拦截H5数据请求,Native侧对应ArkWeb_WebSchemeHandler系列C API,原生用@kit.NetworkKit代发并附加Authorization。这样token全程只在原生侧,H5完全无感知,既没有可窃取的凭据,也不受同源策略约束,token刷新和过期重试还能由原生统一管理。代价是所有H5请求都要过原生一层,需要处理请求体解析、流式响应、文件下载等边界,不适合有大量媒体流或第三方SDK自由发请求的页面。

如果把凭据落到Web内核,还要保证失效联动,清理的位置和顺序都要对,见下一节。

五、能力开放与失效传播

“H5点个按钮调起原生页”这类需求,最省事的实现是开放一个goToAppPage(pageUrl)通用路由,问题在于它把“能跳哪些页”的决定权交给了H5。正确做法是暴露意图方法,由原生决定路由,并对入参做schema校验:
  1. // 反例:决定权在H5手里
  2. goToAppPage(pageUrl: string, params?: ESObject) {
  3.   RouterManager.push({ builderName: pageUrl, param: params });
  4. }
  5. // 正例:决定权在原生手里
  6. openMyOrders(): void {
  7.   RouterManager.push({
  8.     routerName: RouterConstants.ENTRY_HAP,
  9.     builderName: 'mall:StoreMyOrderPage', // 常量,不接受H5入参
  10.   });
  11. }
  12. openProductDetail(skuId: string): void {
  13.   if (!/^[A-Za-z0-9_-]{1,32}$/.test(skuId)) {
  14.     return; // 参数schema校验,静默拒绝
  15.   }
  16.   RouterManager.push({
  17.     routerName: RouterConstants.ENTRY_HAP,
  18.     builderName: 'mall:MallStoreDetailsPage',
  19.     param: { skuId: skuId },
  20.   });
  21. }
复制代码

好处是H5不需要知道原生路由表长什么样,路由重构不会打断线上H5。

失效传播要三层联动,顺序不能反:①调服务端登出接口让会话立即作废(安全层,根本保障);②清WebView Cookie让后续请求不再携带(本地层);③runJavaScript通知H5立即跳未登录页(体验层)。先清本地再调服务端,中间会有一个窗口期凭据仍然有效;如果凭据已通过Referer等途径复制出去,只有服务端作废能真正断掉。

多数实现只覆盖“用户主动退出”,还有三处必须补齐。第一处是被动踢下线:token在别处登录被挤下线、服务端主动作废时用户根本没点退出,只监听退出按钮会让H5一直“看起来已登录”,工程里通常已有踢下线事件(如APP_LOGIN_KICK_OUT一类广播),缺的是把它接到WebView。第二处是H5侧本地缓存要一起清,只清Cookie不通知页面,会出现“Cookie没了但页面还显示已登录、点按钮报401”的割裂状态:
  1. window.onNativeLogout = function (reason) {
  2.   localStorage.clear();
  3.   sessionStorage.clear();
  4.   if (window.caches) {
  5.     caches.keys().then(keys => keys.forEach(k => caches.delete(k)));
  6.   }
  7.   location.replace('/mall/login-required?reason=' + reason);
  8. };
复制代码

第三处是幂等与竞态:主动退出、被动踢下线、网络层401三个入口可能在几秒内同时触发,扇出动作必须幂等,第①步不能省。

另外两个容易被当成边缘情况的主流程要特别注意。多WebView实例下,clearAllCookiesSync()是全局的,若活动页、客服页、支付中间页各自依赖不同域的Cookie,一刀清空会把它们一起登出且难以归因,更精准的是会话Cookie配clearSessionCookieSync()。但ArkTS侧没有按域、按名删除Cookie的接口,清理粒度只有“会话”和“全部”两档,所以域的规划要在设计期做:登录态统一用会话Cookie,确需强隔离的独立业务可用incognito隐私模式WebView(Cookie只存内存、组件销毁即清,但要接受不落盘、页面重开需重新注入的代价)。切换账号不是登出而是换身份,要求“清旧态+注新态”原子完成,可行做法是换号时销毁并重建WebView,或用一次性票据让H5在新会话建立前完全无凭据;预加载的WebView尤其危险,它注入的是上一个账号的Cookie,复用前必须校验账号一致性。

六、ArkWeb API校准:四类常见误用

①注入时机。网上流传的webview.once('webInited')写法只对“进程内第一个WebView”成立。once()是全局一次性事件,内核完成初始化后第二个及之后的WebView不再触发回调,注入会静默失效(接口说明有明确描述)。而WebCookieManager是所有Web组件共享的全局单例,configCookieSync调用本身就会触发内核初始化。真正需要保证的是“注入发生在该实例的首次请求之前”,更稳的做法是放进组件自己的onControllerAttached,并在loadUrl之前执行,对每个实例都成立:
  1. Web({ src: '', controller: this.controller })
  2.   .onControllerAttached(() => {
  3.     webview.WebCookieManager.configCookieSync(domain, cookieValue);
  4.     this.controller.loadUrl(this.mallUrl);
  5.   })
复制代码

setUrlTrustList也应放在这里——该接口要求controller已关联组件(否则报17100001)。

②刷盘。写入成功不等于已持久化,这是“注入成功但偶发失效”的成因。Cookie每30秒周期性保存到磁盘,configCookieSync只写进内核内存,进程在这30秒内被杀(用户划掉、系统回收)就丢了。要立刻持久化需显式刷盘:
  1. webview.WebCookieManager.configCookieSync(domain, cookieValue);
  2. await webview.WebCookieManager.saveCookieAsync(); // 自API 9起提供
复制代码

版本对应别记错:saveCookieAsync自API 9就有;同步版本saveCookieSync反而是API 15才新增,且会阻塞调用线程直到落盘完成。API 9→11之间废弃的是setCookie/getCookie/deleteEntireCookie/deleteSessionCookie这批旧接口,网上“saveCookieSync已更名”的说法是错的。

③退出清Cookie。选用哪个方法取决于Cookie类型:登录态写成会话Cookie(不带expires/Max-Age)用clearSessionCookieSync(),精准且不影响其他域;写成持久Cookie则会话清理方法清不掉,只能退到clearAllCookiesSync()全量清理,代价是带走其他业务登录态。

④permission白名单。methodList给空数组看似配置了,实际表示没有为任何方法单独配权限,方法级管控并未生效。permission分两层:urlPermissionList是对象级(匹配则授权该对象所有方法),methodList是方法级(针对单个方法收紧)。要给敏感方法单独配:
  1. const BRIDGE_PERMISSION: string = JSON.stringify({
  2.   javascriptProxyPermission: {
  3.     urlPermissionList: [
  4.       { scheme: 'https', host: 'mall.example.com', port: '', path: '' }
  5.     ],
  6.     methodList: [
  7.       {
  8.         methodName: 'getToken',
  9.         urlPermissionList: [
  10.           { scheme: 'https', host: 'mall.example.com', port: '', path: '' }
  11.         ]
  12.       }
  13.     ]
  14.   }
  15. });
复制代码

匹配规则三条:scheme和host精确匹配、不能为空、不支持通配符(通配符是新版本setUrlTrustList才支持);port留空表示不检查端口;path是前缀匹配,留空表示不检查路径。还有一条容易漏的交集原则:方法级最终白名单是对象级与方法级的交集,两侧必须同时配,只配一侧该方法就调不通。

七、八条实现细节

第一,SameSite默认是Lax而不是None。跨站跳转还想携带Cookie要显式设None; Secure,否则Cookie不会发送,这一点要跟H5确认跳转来源后再定。

第二,Cookie每30秒自动落盘,崩溃会丢最近30秒;另外在PC/二合一/平板上,即使会话Cookie也不会被saveCookieAsync()写入磁盘,多设备形态要单独验证。

第三,configCookieSync的“多条Cookie”要分清:官方有示例把多个键值对塞进一次调用,但接口说明和FAQ都明确不支持一次性批量设置,应多次调用。原因是value必须遵循Set-Cookie响应头格式,逗号拼法会让整串被当成一个Cookie的值,实际只写入一个:
  1. // 反例:看着像写了三个,实际只写入一个
  2. configCookieSync(url, 'a=b;c=d;e=f');
  3. // 正例:多个Cookie就多次调用
  4. configCookieSync(url, 'a=b; Path=/; Secure');
  5. configCookieSync(url, 'c=d; Path=/; Secure');
复制代码

附带规则:相同host、path、名称的Cookie会被新值替换;已过期的Cookie不会被存储;带Secure属性时url必须是https://否则写入失败;要覆盖已有的HttpOnly Cookie,value里同样要带HttpOnly属性——这是“Cookie注入后H5还是未登录”最隐蔽的成因之一。

第四,methodList和asyncMethodList不能混。methodList放同步方法,返回值直接回传H5;asyncMethodList放返回Promise的方法,其resolve值才回传。把异步方法误登记到methodList,H5侧await bridge.xxx()的then/await永远不触发,表现为“回调不触发”,极易被误判成“桥没注入”。

第五,动态注册要记得refresh()。用registerJavaScriptProxy()注册后必须调refresh()才生效,否则前端报“对象未定义”。

第六,组件销毁时调deleteJavaScriptRegister(name)释放桥对象,防内存泄漏。

第七,测试用例必须覆盖“未登录态”。很多线上事故是“退出后H5还在用旧态”,而自测时从来都是登录状态——加一条:登录→打开H5→原生退出→返回H5→断言处于未登录。

第八,多端统一抽象一层。iOS WKWebView、Android WebView、HarmonyOS ArkWeb的Cookie同步机制各不相同,抽一个H5SessionBridge接口各端实现,业务只调接口,否则同一套逻辑要在三端各踩一遍坑。

八、可直接贴进评审单的清单

新需求先过三步:归类(这次交互属于哪几类命题)、选通道(涉及凭据查风险矩阵、涉及调用查四层校验)、过清单。14条清单如下。

身份贯通组:URL里没有token、cookie、sessionId、手机号等敏感项(非敏感业务参数可留);凭据若落到内核,带齐HttpOnly+Secure+明确SameSite且URL是https;凭据没通过JSBridge返回值下发到脚本域,确需下发时用一次性票据而非长期token;域关系明确,跨域场景已选定代理或票据方案。

能力开放组:桥对象注册时配置了permission且methodList对敏感方法单独收紧;加载域名有系统级白名单(setUrlTrustList或等价手段)覆盖location.href、302、iframe导航;暴露的是意图方法而非通用路由/builderName透传;所有H5入参做schema校验,能力按读/跳/写/敏感分级,敏感级不下放。

失效传播组:退出、被动踢下线、网络层401三个入口都接到同一套扇出逻辑且动作幂等;顺序是“服务端作废→清内核Cookie→通知页面”不能反向;通知H5时清了localStorage/sessionStorage/Cache API;会话Cookie用会话级清理方法,没有误用全局clearAllCookiesSync()波及其他WebView。

再加两条跨组项,最容易在迭代中被破坏:切换账号、WebView预加载复用前校验了账号一致性;“登录→打开H5→退出→返回H5”有用例覆盖。

九、这套框架什么时候不适用

纯原生页面没有WebView,四条通道不存在,直接走原生鉴权即可。系统浏览器、小程序等不受自己控制的容器,域模型不同,无法注入Cookie也无法注册桥,只能走标准OAuth/深链回跳。请求形态高度不可控的页面(WebRTC、大范围流式下载、三方SDK自由发请求),请求代理方案的维护成本会超过收益,应转向票据方案。完全自有、同域、同发布链路且已有严格XSS防护的H5,框架仍适用,但能力分级可适当放宽,放宽要写进评审记录而不是默认省略。

总而言之,凭据不进脚本域、能力按意图授权、失效扇出到每个副本,这三条原则加上ArkWeb的几处API校准,基本能覆盖混合应用里绝大多数“token放哪儿”“退出后为何没失效”的坑。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: ArkWeb内嵌H5信任边界:Cookie注入时机与失效传播排障清单

这篇整理得很到位,尤其是把“身份贯通”和“失效传播”当成同一件事的两端来看,这个视角比单纯讨论 Cookie 怎么注进去更有价值。选型时看“能否撤销”这一列也很实用,URL 拼凭据和 JSBridge 返回值进脚本域之后就收不回来,这两个坑在评审里经常被忽略。 三点原则和三层联动的顺序我基本认同,服务端先作废确实是根本保障,先清本地再登出会留窗口期。被动踢下线这块也确实容易被漏掉,很多工程只接了退出按钮,服务端挤号之后 H5 还挂着旧界面,体验很割裂。 想请教两个落地细节:一是请求代理方案里,onInterceptRequest 拦截 H5 的 POST 请求时,请求体解析和流式响应你们是怎么处理的,有没有遇到文件下载或大响应体被截断的情况;二是切换账号时预加载的 H5 实例,除了清 Cookie 和通知页面,WebView 实例级的状态(比如桥对象里缓存的用户信息、页面栈)需不需要一起重建,还是靠页面自清就够了。再次感谢,清单可以直接拿去评审用了。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: ArkWeb内嵌H5信任边界:Cookie注入时机与失效传播排障清单

楼主这篇把“身份贯通、能力开放、失效传播”归到同一个信任边界上,这个视角很受用。以前排障时确实容易把 Cookie 注入、JSBridge、退出清理当成三个独立问题,结果就是哪块出问题补哪块,缺少统一收口。 几个点特别有共鸣:一是“凭据方案好不好,关键看回收能力”,这个比只讨论怎么传 token 更接近本质;二是 URL 拼凭据不是“小心点就没事”,而是设计上就在往日志、历史、Referer 里送数据;三是失效传播三层联动的顺序,先服务端作废再清本地再通知页面,之前确实见过先清 Cookie 导致中间窗口期还在生效的实现。被动踢下线没接到 WebView 也是高频坑,页面“看起来已登录”比直接报 401 还难排查。 另外意图方法授权那段写得很实用,`openMyOrders` 这种常量路由加 schema 校验,比开放通用 `goToAppPage` 稳得多,H5 也不用知道原生路由表,后续重构确实省心。请求代理的优缺点也总结得到位,尤其是流式响应、文件下载、第三方 SDK 这些边界,评审时很容易被忽略。 整体是一份能直接拿去对照检查的清单,感谢整理。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: ArkWeb内嵌H5信任边界:Cookie注入时机与失效传播排障清单

这篇把原生和H5之间的信任边界拆得很清楚,尤其是把身份贯通和失效传播当成同一件事的两端,这个视角很实用。实际排障里最容易踩的就是“Cookie注入成功就以为安全”“桥只对当前页面生效”“退出只清Cookie”这几个点,文中把HttpOnly、桥对象的实例级作用域、失效传播的三层顺序和被动踢下线的缺口都点到了,评审时可以直接拿来对照。 请求代理和一次性票据的取舍也讲得比较实在,token不下脚本域确实能砍掉很多攻击面,但代价是对请求类型和流式响应的处理要求高,适合按场景选而不是一刀切。能力开放那组正反例也很有参考价值,把路由决定权留在原生侧、入参做schema校验,比暴露通用跳转接口稳得多。整体是一份能落到评审和排障里的清单。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-6 02:21 , Processed in 0.021288 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部