鸿蒙NEXT开发【应用安全编码实践】开发安全

通用组件安全

建议不涉及对外交互业务的应用组件的exported属性设置为false

只在应用程序内部使用的应用组件必须设置为不可见,以防受到其他应用程序的调用。

不对外交互的应用组件应该明确设置该应用组件的exported属性值为false(若未设置exported属性默认为false,建议显式设置防止遗漏和错误配置)。否则,可能会对外暴露内部接口,若被恶意应用利用,则可能会造成应用组件的信息泄露、功能异常、应用拒绝服务等问题。

错误示例:

// module.json5
"abilities": [ 
  { 
    "name": "PrivacyAbility", 
    "srcEntry": "./ets/privacyability/PrivacyAbility.ts", 
    "description": "$string:PrivacyAbility_desc", 
    "exported": true 
  } 
]

PrivacyAbility是一个不期望对外交互的组件,但是设置exported属性为true,导致任何应用都可以访问该组件。

正确示例:

// module.json5 
"abilities": [ 
  { 
    "name": "PrivacyAbility", 
    "srcEntry": "./ets/privacyability/PrivacyAbility.ts", 
    "description": "$string:PrivacyAbility_desc", 
    "exported": false 
  } 
]

对外交互的应用组件应设置合理的访问权限

应用程序的应用组件不只是在应用程序内部使用,很多情况下需要与外部进行交互。以下情况表明应用组件是对外交互的:

  • module.json5文件中显式设置exported属性为true,则表明该Ability允许对外交互的;

程序内应用组件一旦允许对外交互,数据存在跨信任边界进行交互,若被恶意应用程序利用,可能会导致Ability信息泄露、功能异常、应用拒绝服务等问题;因此,应用程序对外交互的应用组件使用permissions属性来设置访问权限。

常见敏感功能、对外提供的Ability利用风险,建议涉及这些情况的组件设置权限进行保护:

1)未经用户操作确认就可执行关键操作,例如:发送短信、拨打电话、拍照、录音、录屏 、截屏、定位等

2)导致个人数据、敏感数据泄露的关键操作,例如:读取短信、读取联系人、读取设备标识符等

3)导致权限提升,例如:可以进行原本system权限才能进行的操作

例外情况: 必须对外提供且无利用风险的应用组件,可以不设置权限。

错误示例:

// module.json5 
"abilities": [ 
  { 
    "name": "ContactDataAbility", 
    "srcEntry": "./ets/DataAbility/DataAbility.ts", 
    "description": "$string: DataAbility_desc", 
    "exported": true 
  } 
]

可以看出,任意外部应用都可以访问此ContactDataAbility,恶意应用程序可能利用该组件获取联系人数据。

正确示例:

// module.json5 
"abilities": [ 
  { 
    "name": "ContactDataAbility", 
    "srcEntry": "./ets/DataAbility/DataAbility.ts", 
    "description": "$string: DataAbility_desc", 
    "exported": true, 
    "permissions": ["ohos.permission.READ_CONTACTS"] 
  } 
]

建议隐式启动应用组件时避免携带个人数据

隐式want是通过action条件匹配的方式来筛选组件,凡是满足条件的应用组件都有被启动的可能。如果携带个人数据,则恶意应用有可能劫持携带的个人数据。应用程序如果要携带个人数据,需要显式指定目标应用组件(bundle名、ability名)或者将个人数据匿名化。
错误示例:

import { Want } from "@kit.AbilityKit";

let wantInfo:Want = { 
  deviceId: '', 
  action: "ability.want.test", 
  parameters: { 
    "password": "xxxxxxxx" 
  } 
} 
try{ 
  let data = this.context.startAbility(wantInfo) 
  console.info("startAbility success " + JSON.stringify(data)); 
} catch (err) { 
  console.error("startAbility  with error message: " + err.message + ", error code: " + err.code); 
}

通过示例代码中的action:"ability.want.test"可以隐式启动组件,如果恶意应用也声明了该action,隐式调用时会出现一个列表让用户选择,如果用户进入了恶意应用,则恶意应用可以获取传递的敏感信息。

正确示例:

import { Want } from "@kit.AbilityKit";

let wantInfo:Want = { 
  deviceId: '', 
  action: "ability.want.test", 
  bundleName:'com.example.myapplication10', 
  abilityName:'MainAbility1', 
  parameters: { 
    "password": "xxxxxxxx" 
  } 
} 
try{ 
  let data = this.context.startAbility(wantInfo) 
  console.info("startAbility success " + JSON.stringify(data)); 
} catch (err) { 
  console.error("startAbility  with error message: " + err.message + ", error code: " + err.code); 
}

这里显式指定了需要拉起的应用组件的bundleName和abilityName,只会拉起指定的目标组件。

避免涉及口令输入的应用界面可以被截屏或录屏

口令输入界面主要涉及账号密码输入框及输入法弹出框,在用户输入口令时,应避免截屏或录屏操作。否则,一旦恶意软件骗取用户信任获得授权,窃取到用户操作的账号和密码,即可在其他设备中登录,造成用户隐私泄露。

实施指导:

在需要保护的Page页面调用setWindowPrivacyMode禁止截屏/录屏。

注意:此接口需要申请ohos.permission.PRIVACY_WINDOW权限。

正确示例:

在口令输入、转账支付页面中设置隐私属性,防止截屏/录屏:

import { router, window } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  onPageShow(): void {
    window.getLastWindow(getContext(this)).then((windowStage: window.Window) => {
      windowStage.setWindowPrivacyMode(true);
    });
  }
  onPageHide(): void {
    window.getLastWindow(getContext(this)).then((windowStage: window.Window) => {
      windowStage.setWindowPrivacyMode(false);
    });
  }
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button('click to start Succ Page')
          .onClick(async () => {
            router.pushUrl({ url: 'pages/loginSuccPage' })
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

公共事件安全

避免使用携带个人数据未设置权限的动态公共事件

不同应用程序间可以使用公共事件进行进程间通信,如果公共事件发送权限设置不当,且携带个人数据,任意应用就可以读取该个人数据,造成用户数据泄露。

对于携带个人数据的公共事件,需要设置公共事件发送权限或者将个人数据加密。

错误示例:

import { commonEventManager } from '@kit.BasicServicesKit';

function publishEventWithData() {
  let options: commonEventManager.CommonEventPublishData = {
    code: 1,
    data: "ContactData", // 带敏感数据发送
  }
  commonEventManager.publish("MyCommonEvent", options, (err) => {
    if (err.code) {
      console.error("publish event error: " + err.code + ", " + err.message + ", " + err.name + ", " + err.stack);
    } else {
      console.info("publish event with data Succeeded");
    }
  })
}

发送了一个带个人数据(示例中假设是联系人数据)的公共事件,但是没有设置接收方接受此公共事件所需要的权限。

正确示例:

import { commonEventManager } from '@kit.BasicServicesKit';

function publishEventWithData() {
  let options: commonEventManager.CommonEventPublishData = {
    code: 1,
    data: "ContactData", // 带敏感数据发送
    subscriberPermissions: ["ohos.permission.READ_CONTACTS"], // 设置权限
  }
  commonEventManager.publish("MyCommonEvent", options, (err) => {
    if (err.code) {
      console.error("publish event error: " + err.code + ", " + err.message + ", " + err.name + ", " + err.stack);
    } else {
      console.info("publish event with data Succeeded");
    }
  })
}

通过subscriberPermissions字段设置了接收公共事件的权限,这样,发布者要求订阅者必须具有“ohos.permission.READ_CONTACTS”权限,才能接收该携带联系人数据的公共事件。

建议对涉及敏感功能的公共事件进行访问权限控制

每个应用都支持订阅自定义的公共事件,并在接收到公共事件后执行一定的功能。应用在订阅公共事件后,通过publisherPermission设置订阅公共事件接收的权限。如果订阅的是自定义的公共事件且未设置权限,任意应用可以发送同名的自定义的公共事件,从而可能导致接收公共事件的应用拒绝服务甚至敏感功能泄露。

对于涉及敏感数据/操作的公共事件:订阅方需要设置权限且权限等级足够高。

错误示例:

import { commonEventManager } from '@kit.BasicServicesKit';

let subscriber: commonEventManager.CommonEventSubscriber;
// 订阅者信息
let subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
  events: ["event"]
};

// 创建订阅者
try {
  commonEventManager.createSubscriber(subscribeInfo, (err, commonEventSubscriber) => {
    if (!err) {
      console.info("createSubscriber Succeed");
      subscriber = commonEventSubscriber;
      // 订阅公共事件
      try {
        commonEventManager.subscribe(subscriber, (err, data) => {
          if (err) {
            console.error(`subscribe failed, code is ${err.code}, message is ${err.message}`);
          } else {
            // 接收公共事件后事件处理
            // doSomeDangrousThing(data)
          }
        });
      } catch (err) {
        console.error(`subscribe failed, code is ${err.code}, message is ${err.message}`);
      }
    } else {
      console.error(`createSubscriber failed, code is ${err.code}, message is ${err.message}`);
    }
  });
} catch (err) {
  console.error(`createSubscriber failed, code is ${err.code}, message is ${err.message}`);
}

正确示例:

import { commonEventManager } from '@kit.BasicServicesKit';

let subscriber: commonEventManager.CommonEventSubscriber;
// 订阅者信息
let subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
  events: ["event"],
  publisherPermission: "ohos.permission.publisherPermission1", // 设置订阅权限
};

// 创建订阅者
try {
  commonEventManager.createSubscriber(subscribeInfo, (err, commonEventSubscriber) => {
    if (!err) {
      console.info("createSubscriber");
      subscriber = commonEventSubscriber;
      // 订阅公共事件
      try {
        commonEventManager.subscribe(subscriber, (err, data) => {
          if (err) {
            console.error(`subscribe failed, code is ${err.code}, message is ${err.message}`);
          } else {
            // 接收公共事件后事件处理
            // doSomeDangrousThing(data)
          }
        });
      } catch (err) {
        console.error(`subscribe failed, code is ${err.code}, message is ${err.message}`);
      }
    } else {
      console.error(`createSubscriber failed, code is ${err.code}, message is ${err.message}`);
    }
  });
} catch (err) {
  console.error(`createSubscriber failed, code is ${err.code}, message is ${err.message}`);
}

常见敏感功能、对外提供的公共事件利用风险,建议涉及这些情况的公共事件设置权限进行保护:

1)未经用户操作确认就可执行关键操作,例如:发送短信、拨打电话、拍照、录音、录屏 、截屏、定位等

2)导致个人数据、敏感数据泄露的关键操作,例如:读取短信、读取联系人、读取设备标识符等

3)导致权限提升,例如:可以进行原本system权限才能进行的操作

例外情况: 必须对外提供且无利用风险的应用公共事件,可以不设置权限。

WebView安全

避免加载不安全的URL或页面

Web组件或WebController均可以加载URL或页面。如果加载的URL或页面可被攻击者控制,则可加载恶意JS代码,使得恶意代码可以调用ArkTS开放的敏感JS接口,获取用户个人数据,或者对应用进行攻击等。

因此,需要在加载URL之前,通过Web组件的onLoadIntercept方法拦截到要加载的URL,准确获取URL各个字段值,并校验获取的值是否在业务预置的白名单内

正确示例:

import { uri } from '@kit.ArkTS';
import { webview } from '@kit.ArkWeb';

// 校验函数
function checkUrl(str: string): boolean {
  let tmpUri = new uri.URI(str);
  let res = tmpUri.normalize(); // 注意,需要先对tmpUri进行normalize,否则会绕过安全校验  
  console.info("res.scheme:" + res.scheme) // 协议  
  console.info("res.host:" + res.host) // 域名
  console.info("res.port:" + res.port) // 端口  
  console.info("res.path:" + res.path) // 路径 
  console.info("res.ssp:" + res.ssp)   
  // 获取到scheme、host、port、path等参数值后,根据业务实际进行安全校验  
  // 此处省略部分,根据业务需要进行校验  
  if ("校验成功") {
    return true;
  }
  return false
}

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Button("loadUrl")
        .onClick(() => {
          this.controller.loadUrl('www.huawei.com')
        })
        .margin({ top: 50 })
      Web({ src: 'www.huawei.com', controller: this.controller })
        .onLoadIntercept((event) => {
          console.info('onLoadIntercept:' + event.data.toString())
          let tempUrl = event.data.toString()
          return checkUrl(tempUrl) // 返回true表示阻止此次加载,否则允许此次加载
        })
    }
  }
} 

例外: 如果应用本身是提供URL加载能力的,譬如浏览器等,可以例外。但是要注意,如果应用也提供了敏感的JS接口,要注意防范可能的被恶意调用的风险。

避免加载不可信的javascript脚本

WebController提供了runJavaScript和runJavaScriptExt函数用于异步执行JavaScript脚本,并通过回调方式返回脚本执行的结果。

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .javaScriptAccess(true)
        .onPageEnd(e => {
          this.controller.runJavaScript('test()')
            .then((result) => {
              console.log('result: ' + result);
            })
            .catch((error: BusinessError) => {
              console.error(`ErrorCode: ${error.code},  Message: ${error.message}`);
            })
          if (e) {
            console.info('url: ', e.url);
          }
        })
    }
  }
}

加载的index.html如下:

<!DOCTYPE html>  
<html>  
  <meta charset="utf-8">  
  <body>  
      Hello world!  
  </body>  
  <script type="text/javascript">  
  function test() {  
      console.log('Ark WebComponent')  
      return "This value is from index.html"  
  }  
  </script>  
</html>   

如果加载的脚本内容不可信,则可加载恶意JavaScript代码,造成XSS。如果Web组件注册了敏感JavaScript接口,还会导致JavaScript接口被恶意调用,影响应用安全性。

因此,若待加载的JavaScript脚本外部可控,则需要在加载前进行白名单校验。

正确示例:

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .javaScriptAccess(true)
        .onPageEnd(e => {
          let whiteMethods = ["test()", "test1()"]
          let jsMethod: string = "alert(`xss`)" // 外部可控字段
          // 白名单校验
          if (whiteMethods.indexOf(jsMethod) === -1) {
            console.error("input method not in whiteList")
            return
          }
          this.controller.runJavaScript(jsMethod)
            .then((result) => {
              console.log('result: ' + result);
            })
            .catch((error: BusinessError) => {
              console.error(`ErrorCode: ${error.code},  Message: ${error.message}`);
            })
          if (e) {
            console.info('url: ', e.url);
          }

        })
    }
  }
}   

错误示例:

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .javaScriptAccess(true)
        .onPageEnd(e => {
          let jsMethod: string = "alert(`xss`)" // 外部可控字段
          this.controller.runJavaScript(jsMethod)
            .then((result) => {
              console.log('result: ' + result);
            })
            .catch((error: BusinessError) => {
              console.error(`ErrorCode: ${error.code},  Message: ${error.message}`);
            })
          if (e) {
            console.info('url: ', e.url);
          }
        })
    }
  }
}   

避免将mixedMode属性配置成All

mixedMode设置是否允许加载超文本传输协议(HTTP)和超文本传输安全协议(HTTPS)混合内容,默认不允许加载HTTP和HTTPS混合内容。支持三种模式,其中All是允许混合内容加载,存在中间人攻击的风险,默认不允许配置成All。

错误示例:

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({ src:"www.huawei.com", controller: this.controller })
        .mixedMode(MixedMode.All)
    }
  }
}

应该去掉此配置项,或者选择MixedMode.None模式。

例外: 对于提供特定业务的应用可以例外,譬如浏览器。

避免在SSL校验出错时继续加载页面

onSslErrorEventReceive回调函数用于通知用户加载资源时发生SSL错误。

当Web组件通过SSL协议进行网络请求时,底层Web组件校验服务端返回的证书或者协议等错误时,默认情况下要cancel请求,否则存在中间人攻击风险。

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()
  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .onSslErrorEventReceive((event) => {
          console.info('ssl check failed,error is : ' + event.error.toString())
          event.handler.handleCancel();
        })
    }
  }
}

错误示例:

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .onSslErrorEventReceive((event) => {
          event.handler.handleConfirm();
        })
    }
  }
}

如果在SSL校验出错时,直接调用handleConfirm函数,则页面会忽略SSL错误继续加载,会导致中间人攻击等风险。

例外: 对于浏览器等用于加载URL的应用可以例外,但是也需要在页面显式告知用户待加载页面存在安全风险。

避免在用户同意前返回位置信息

geolocationAccess开关用于配置是否开启地理位置权限,默认开启。onGeolocationShow回调函数用于通知用户收到地理位置信息获取请求。H5页面请求获取地理位置信息时,Web组件通过上述两个API进行配置。

无使用场景时,需要显式禁用geolocationAccess。存在使用场景时,需要在onGeolocationShow回调内先弹框提示用户,并经过用户确认和同意才可以返回位置信息,否则存在隐私泄露风险。

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .geolocationAccess(true)
        .onGeolocationShow((event) => {
          if (event === undefined) {
            return
          }
          AlertDialog.show({
            title: 'title',
            message: 'text',
            confirm: {
              value: 'onConfirm',
              action: () => {
                event.geolocation.invoke(event.origin, true, true);
              }
            },
            cancel: () => {
              event.geolocation.invoke(event.origin, false, true);
            }
          })
        })
    }
  }
}

错误示例:

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct WebComponent {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .geolocationAccess(true)
        .onGeolocationShow((event) => {
          if (event === undefined) {
            return
          }
          event.geolocation.invoke(event.origin, true, true);
        })
    }
  }
}

在有位置权限的使用场景时,不能直接返回位置信息。需要明示给用户且用户同意后,方可返回。

避免注册返回含有全局认证凭据的JavaScriptProxy

在HarmonyOS中,可以使用Web组件加载H5等页面。同时Web组件可以通过JavaScriptProxy方法或者通过WebController的registerJavaScriptProxy方法向H5提供JS接口,供H5访问。

如果提供的JS接口返回了敏感信息或有敏感操作,被恶意调用,则存在安全风险。尤其是提供的JS接口返回了用于认证用户身份的全局认证凭据ServiceToken(下述简称ST)或者AccessToken(下述简称AT),由于是全局认证凭据,攻击者拿到此全局ST或AT,可以冒充用户恶意调用此应用在云端的所有接口,使攻击影响扩大化。因此需禁止Web组件提供获取全局认证凭据ST或AT的JavaScriptProxy。

错误示例:

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct Index {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Row() {
        Button('registerJavaScriptProxy').onClick(() => {
          this.controller.registerJavaScriptProxy(
            {
              getServiceToken: () => {
                return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
              }
            },
            "objName",
            ["getServiceToken"],
          );
          this.controller.refresh()
        })
      }

      Web({ src: $rawfile('H5CallETS.html'), controller: this.controller })
        .javaScriptAccess(true)
        .javaScriptProxy({
          object: {
            getServiceToken: () => {
              return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
            }
          },
          name: "objName",
          methodList: ["getServiceToken"],
          controller: this.controller
        })
    }
  }
}

H5攻击界面:

<!DOCTYPE html>  
<html>  
<meta charset="utf-8">  
<body>  
Hello js call ets interface!  
<br>  
<script type="text/javascript">  
    function callJSInt() {  
        let ST = objName.getServiceToken();  
        console.log('wzz:ST = '+ ST);  
        alert(ST);  
    }  
</script>  
<button onclick="callJSInt()">get ST</button>  
</body>  
</html> 

如上所示,提供了获取全局认证凭据的ST接口供H5调用,如果H5存在漏洞,攻击者拿到此ST即可仿冒用户访问用户的云服务上的个人数据,进行恶意操作等。

为了降低用户的全局认证凭据ST或AT丢失带来的安全风险,推荐两种修改方法如下:

第一种方法: 在加载URL页面前,校验当前页面的URL是否在白名单内,仅允许白名单内的URL调用。要同时校验协议、域名、PATH等,白名单范围控制的越小越安全。

校验方法可使用如下API格式化相关URL,跟预置的数据进行比较。

  1. 使用URI库进行URL格式化 准确获取URL各个字段值,并校验获取的值是否在业务预置的白名单内
// Index.ets
import { uri } from '@kit.ArkTS';
import { webview } from '@kit.ArkWeb';

// 校验函数
function checkUrl(suri: string): boolean {
  let tmpUri: uri.URI = new uri.URI(suri);
  let res = tmpUri.normalize(); // 注意,需要先对tmpUri进行normalize,否则会绕过安全校验
  console.info("res.scheme:" + res.scheme) // 协议
  console.info("res.host:" + res.host) // 域名
  console.info("res.port:" + res.port) // 端口
  console.info("res.path:" + res.path) // 路径
  console.info("res.ssp:" + res.ssp)
  // 获取到scheme、host、port、path等参数值后,根据业务实际进行安全校验
  // 此处省略部分,根据业务需要进行校验
  if ("校验成功") {
    return true;
  }
  return false
}

@Entry
@Component
struct Index {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Row() {
        Button('registerJavaScriptProxy').onClick(() => {
          this.controller.registerJavaScriptProxy(
            {
              getServiceToken: () => {
                return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
              }
            },
            "objName",
            ["getServiceToken"],
          );
          this.controller.refresh()
        })
      }

      Web({ src: $rawfile('H5CallETS.html'), controller: this.controller })
        .javaScriptProxy({
          object: {
            getServiceToken: () => {
              return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
            }
          },
          name: "objName",
          methodList: ["getServiceToken"],
          controller: this.controller
        })
        .onLoadIntercept((event) => {
          console.info('onLoadIntercept:' + event.data.toString())
          let tempUrl = event.data.toString()
          return checkUrl(tempUrl); // 返回true表示阻止此次加载,否则允许此次加载
        })
    }
  }
}
  1. 使用URL库进行URL格式化 准确获取URL各个字段值,并校验获取的值是否在业务预置的白名单内
// Index.ets
import { url } from '@kit.ArkTS';
import { webview } from '@kit.ArkWeb';

// 校验函数
function checkUrl(surl: string): boolean {
  let tmpUrl = url.URL.parseURL(surl);
  console.info("res.scheme:" + tmpUrl.protocol) // 协议
  console.info("res.host:" + tmpUrl.host) // 域名
  console.info("res.port:" + tmpUrl.port) // 端口
  console.info("res.path:" + tmpUrl.pathname) // 资源路径
  console.info("res.ssp:" + tmpUrl.hostname) // 域名
  // 获取到scheme、host、port、path等参数值后,根据业务实际进行安全校验
  // 此处省略部分,根据业务需要进行校验
  if ("校验成功") {
    return true;
  }
  return false
}

@Entry
@Component
struct Index {
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      Row() {
        Button('registerJavaScriptProxy').onClick(() => {
          this.controller.registerJavaScriptProxy(
            {
              getServiceToken: () => {
                return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
              }
            },
            "objName",
            ["getServiceToken"]
          );
          this.controller.refresh()
        })
      }

      Web({ src: $rawfile('H5CallETS.html'), controller: this.controller })
        .javaScriptAccess(true)
        .javaScriptProxy({
          object: {
            getServiceToken: () => {
              return "DLFJSLDFJALGJLDFJSDDFISLDF323LSDJFLS212DLSFJOEV";
            }
          },
          name: "objName",
          methodList: ["getServiceToken"],
          controller: this.controller
        })
        .onLoadIntercept((event) => {
          console.info('wzz:onLoadIntercept:' + event.data.toString())
          let tempUrl = event.data.toString()
          return checkUrl(tempUrl); // 返回true表示阻止此次加载,否则允许此次加载         
        })
    }
  }
}

第二种方法: 针对H5页面申请一个新的H5Token,并严格限定此H5Token可以调用的云服务业务接口在最小范围内。

数据传递安全

建议对跨信任边界传入的Want进行合法性判断

攻击者向指定应用Ability发送空的Want或携带恶意数据,如果应用接收该Want,却没有进行合法性判断,可能会导致应用业务逻辑被篡改,数据泄露、财产损失等问题。因此,应对外部传入的Want内容进行合法性判断。

常见的对Want的判断:

  1. 使用Want的数据属性前判null,判undefined,确保访问的对象属性存在。
import { hilog } from '@kit.PerformanceAnalysisKit';
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';

// 校验函数
function checkWant(want: Want) {
  if (want === null || want.parameters === null
    || want === undefined || want.parameters === undefined) {
    return false;
  }
  return true;
}

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    if (checkWant(want)) {
      console.info("invalid want");
    } else {
      console.info("correct want");
    }
  }
}
  1. 使用Want的数据前进行try…catch异常捕获,防止应用崩溃,同时可以定位问题。
import { hilog } from '@kit.PerformanceAnalysisKit';
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';

// 校验函数
function checkWant(want: Want) {
  try {
    if (want.parameters) {
      let param = want.parameters["test"] as Want;
      let str = param.uri
      console.info("get uri string: " + str);
    }
  } catch (e) {
    return false
  }
  return true;
}

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    if (checkWant(want)) {
      console.info("invalid want");
    } else {
      console.info("correct want");
    }
  }
}

应用获取外部不可信Want的方式有很多种,例如:

在不同场景下获取Want的方式如下:

// 应用生命周期类:
// @ohos.application.AbilityStage.d.ts  
onAcceptWant(want: Want): string;  
// ability生命周期类:
// @ohos.application.Ability.d.ts  
onCreate(want: Want, param: AbilityConstant.LaunchParam): void;  
onContinue(wantParam : {[key: string]: any}): AbilityConstant.OnContinueResult;  
onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void;  
// ServiceExtensionAbility生命周期类:
// @ohos.application.ServiceExtensionAbility.d.ts  
onCreate(want: Want): void;  
onRequest(want: Want, startId: number): void;  
onConnect(want: Want): rpc.RemoteObject;  
onDisconnect(want: Want): void;  
onReconnect(want: Want): void;  

数据存储安全

避免直接使用不可信数据来拼接SQL语句

SQL注入是指对用户输入数据的合法性没有判断或过滤不严,攻击者可以传入特定内容将应用事先定义好的语句变成含义完全不同的SQL语句,导致信息泄露或者数据篡改。

任何外部的输入包括用户的输入都是不安全的,需要对外部输入的数据进行过滤处理。

HarmonyOS系统提供的关系型数据库是一款基于SQLite组件的数据库,可以直接运行用户输入的SQL语句。如果不可信的数据会被用于拼接SQL语句,就存在SQL注入的风险。应该使用参数化查询的方式或者对不可信内容做过滤。

HarmonyOS上querySql、executeSql会被用于拼接SQL语句的执行。

错误示例:

import { BusinessError } from '@kit.BasicServicesKit';

import { relationalStore } from '@kit.ArkData'

// sql参数来自外部输入
function exesql(sql: string) {
  const STORE_CONFIG: relationalStore.StoreConfig = {
    name: "RdbTest.db",
    securityLevel: relationalStore.SecurityLevel.S1
  };
  let store: relationalStore.RdbStore | undefined = undefined;
  relationalStore.getRdbStore(this.context, STORE_CONFIG, (err: BusinessError, rdbStore: relationalStore.RdbStore) => {
    store = rdbStore;
    if (err) {
      console.error(`Get RdbStore failed, code is ${err.code},message is ${err.message}`);
      return;
    }
    console.info('Get RdbStore successfully.');
  })
  // 使用外部输入直接拼接sql语句,未校验
  let SQL_DELETE_TABLE = "DELETE FROM test WHERE name = " + sql;
  if (store != undefined) {
    (store as relationalStore.RdbStore).executeSql(SQL_DELETE_TABLE, (err) => {
      if (err) {
        console.error(`ExecuteSql failed, code is ${err.code},message is ${err.message}`);
        return;
      }
      console.info('Delete table done.');
    })
  }
}

如果直接使用用户输入作为查询的参数,恶意用户可以输入类似 1’ OR ‘a’ = 'a 的字符串,使sql语句拼接变成select *from book where name=‘1’ OR ‘a’ = ‘a’,从而导致SQL注入攻击。因此禁止直接使用外部输入作为查询字符串的一部分,或者在拼接SQL语句之前,对外部输入进行字符输入校验。

避免将个人数据存放到剪贴板中

剪贴板是指操作系统提供的一个暂存数据,并且提供共享的一个模块,也称为数据中转站。剪贴板在后台起作用,存放在内存里。

如果把个人数据放入剪贴板中是不安全的,因为任意程序都可以访问剪贴板中的内容。

将内容存放入剪贴板的方法有SystemPasteboard类中的setPasteData方法。

例外: 如果是用户自己主动以剪切板作为中转,拷贝数据,则不受此规则约束。

避免使用未校验的外部数据拼接文件路径

HarmonyOS系统对文件存储进行了沙箱隔离,内部存储的文件只有应用程序有权限读取,且只能读取本应用内部存储目录;而对外部共享存储进行读取,则需要添加读取权限

在对文件进行读、写、下载、删除等操作时,如果使用了外部输入来拼接文件路径,攻击者可以注入…/等恶意字符进行跨目录攻击,可导致数据泄露、应用拒绝服务等安全问题。因此在拼接路径之前,需要校验不可信数据是否包含了恶意的攻击字符。

// fileName来自外部
function readFile(fileName: string) {
  let filePath = "/data/storage/";
  if (fileName.indexOf("..") === -1) { // 防拼接校验  
    let fileAllPath: string = filePath + fileName
    // 业务处理  
  } else {
    console.info("be attacked")
    return
  }
}  

配置安全

建议正确设置发布版本应用调试属性

"debug"属性是为了方便开发人员对应用进行调试,但对于正式发布版本,如果应用设置为可调式模式,方便了攻击者对应用进行更深入的分析调试,不利于对应用的保护,因此要求正式版本须设置为不可调试模式。

对"debug"的设置需要在app.json5文件中进行配置。其中"debug"属性缺省值为false。

错误示例:

// app.json5   
{  
    "app": {  
        "bundleName": "com.application.music",  
        "vendor": "application",  
        "versionCode": 1,  
        "versionName": "1.0",  
        "minCompatibleVersionCode": 1,  
        "minAPIVersion": 7,  
        "targetAPIVersion": 8,  
        "apiReleaseType": "Release",  
        "debug": true, // debug字段设置 
        "icon": "$media:app_icon",  
        "label": "$string:app_name",  
        "description": "$string:description_application",  
        "distributedNotificationEnabled": true,  
        "entityType": "game",  
        "car": {  
            "apiCompatibleVersion": 8  
        }  
    }  
} 

正确示例:

// app.json5   
{  
    "app": {  
        "bundleName": "com.application.music",  
        "vendor": "application",  
        "versionCode": 1,  
        "versionName": "1.0",  
        "minCompatibleVersionCode": 1,  
        "minAPIVersion": 7,  
        "targetAPIVersion": 8,  
        "apiReleaseType": "Release",  
        "debug": false,  
        "icon": "$media:app_icon",  
        "label": "$string:app_name",  
        "description": "$string:description_application",  
        "distributedNotificationEnabled": true,  
        "entityType": "game",  
        "car": {  
            "apiCompatibleVersion": 8  
        }  
    }  
}  

建议发布的软件包进行代码混淆

鸿蒙应用软件包(HAP、HAR、HSP)等可被反编译。配置代码混淆,给攻击者软件代码的分析设置障碍,可以增加其分析成本。

正确示例,开启属性混淆例子:

# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
#   https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md
   
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# 开启对象属性混淆
-enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file

# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope   


在应用源码工程obfuscation-rules.txt文件中,按照应用业务的需求配置和开启混淆规则。

建议应用使用的依赖库处于最新状态

大多数应用使用外部三方依赖库和系统SDK依赖来完成特定的任务。通过及时更新应用的依赖项,可以提高应用的安全性。

在发布您的应用前,建议检查所有库、SDK 和其他依赖项都处于最新状态:

  • 对于 SDK 等第一方依赖项,请使用 DevEco Studio开发工具中提供的SDK管理工具
  • 对于第三方依赖项,请检查您的应用所用库的网站,并安装所有可用的更新和安全补丁。

应用签名安全

避免正式版本应用使用debug调试签名

HarmonyOS应用打包时,可以选择debug和release模式。Debug的应用是为了方便开发人员对应用进行调试。但在正式发布版本,如果应用设置为可调试模式,方便了攻击者对应用进行更深入的分析调试,不利于对应用的保护,因此要求正式版本上发布的应用禁止使用debug签名。

注意:目前IDE默认编译hap应用均为debug模式应用,不可直接用于版本商用

建议应用需保证签名完整性

应用签名是保障应用完整性的重要手段,因为要根据版本对应用进行资源裁剪、字节调整等原因,可能会在签名后再对应用进行修改,修改后未重新签名直接预装到手机上,类似行为会破坏应用签名完整性。

禁止应用破坏签名后上架或者直接预装到版本中。

建议应用在申请应用证书时不使用个人信息

应用签名必须要申请签名证书,申请证书时必须输入开发者信息、公司信息等,该证书是应用的身份标识和认证凭据。申请的证书在签名时会嵌入到应用包,成为应用包数据结构的一部分,而开发者可以通过公开的API或者读取应用包数据结构读取到应用证书信息。

如果开发者在证书信息中填入自己的姓名、工号等个人信息,会造成一定程度的信息泄露,也容易被质疑。证书信息应填写公司、团队名称,而非个人信息。

建议应用软件应包含的签名信息需要真实有效

预置应用软件应包含签名信息,且签名信息真实有效,对于非开源鸿蒙官方应用,不得采用开源鸿蒙公开证书,也不能使用明显与该产品无关的签名信息如Demo等;应用开发者,公司和部门每一项需保证真实有效,不能为网址、乱码等无效信息,签名证书中宜正确标识开发者身份主体信息,如企业名称、组织、省市和国家等信息,不得含有跟主体无关的信息。

错误示例:

10-11 17:51:53 WARN  - Missing parameter: outproof
10-11 17:51:53 INFO  - Find Hap Signing Block success, version: 3, block count: 2
10-11 17:51:54 INFO  - +++++++++++++++++++++++++++certificate #0 +++++++++++++++++++++++++++++++
10-11 17:51:54 INFO  - Subject: C=CN, O=Organization, OU=Unit, CN=ide_demo_app // CN字段无法标识开发组织
10-11 17:51:54 INFO  - Issuer: CN=OpenHarmony Application CA, OU=OpenHarmony Team, O=OpenHarmony, C=CN       

正确示例:

10-11 17:51:54 INFO  - Issuer: CN=OpenHarmony Application CA, OU=OpenHarmony Team, O=OpenHarmony, C=CN  
10-11 17:52:21 WARN  - Missing parameter: outproof
10-11 17:52:21 INFO  - Find Hap Signing Block success, version: 3, block count: 2
10-11 17:52:21 INFO  - +++++++++++++++++++++++++++certificate #0 +++++++++++++++++++++++++++++++
10-11 17:52:21 INFO  - Subject: CN=应用名称, OU=开发者组织/公司/部门, O=开发者组织/公司/部门, C=CN // CN字段标识开发组织
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值