跳过内容

Android 平台 API

概述

应用权限

Android 为每个已安装的应用分配一个独立的系统身份(Linux 用户 ID 和组 ID)。由于每个 Android 应用都在一个进程沙箱中运行,因此应用必须显式请求对其沙箱之外的资源和数据的访问权限。它们通过声明所需的权限来请求使用系统数据和功能。根据数据或功能的敏感性或关键性,Android 系统将自动授予权限或要求用户批准请求。

为了增强用户隐私并降低隐私风险,Android 应用最大限度地减少权限请求,并且仅在绝对必要时才请求访问敏感信息,这一点至关重要。Android 开发者文档提供了宝贵的见解和最佳实践,以帮助应用在无需直接访问敏感资源的情况下实现相同的功能级别

Android 权限可以根据其授予应用的受限数据访问和允许操作的范围,分为不同的类别。此分类包括所谓的“保护级别”,如权限 API 参考页面AndroidManifest.xml 源定义所示。

  • 安装时权限:授予对受限数据的有限访问权限,或允许应用执行对系统或其他应用影响最小的受限操作。它们在安装时(Android 6.0 (API 级别 23) 或更高版本)自动授予。
    • 保护级别:normal。授予应用对隔离的应用级功能的访问权限,对其他应用、用户和系统造成的风险最小。示例:android.permission.INTERNET
    • 保护级别:signature。仅授予与声明应用使用相同证书签名的应用。示例:android.permission.ACCESS_MOCK_LOCATION
    • 保护级别:signatureOrSystem。保留给系统嵌入式应用或与声明应用使用相同证书签名的应用。示例:android.permission.ACCESS_DOWNLOAD_MANAGERsignature|privileged 的旧同义词。在 API 级别 23 中已弃用。
  • 运行时权限:需要在运行时提示用户进行明确批准。
    • 保护级别:dangerous。授予对受限数据的额外访问权限,或允许应用执行对系统和其他应用影响更大的受限操作。
  • 特殊权限:要求用户导航到设置 > 应用 > 特殊应用访问并给予明确同意。
    • 保护级别:appop。授予对特别敏感的系统资源的访问权限,例如在其他应用上显示和绘制或访问所有存储数据。
  • 自定义权限,以便与其他应用共享其自身资源和功能。
    • 保护级别:normalsignaturedangerous

独立于所分配的保护级别,考虑权限可能带来的风险非常重要,尤其是考虑到额外的受保护功能,这对于预装应用尤为重要。下表显示了一组代表性的 Android 权限,根据此论文中定义的关联风险进行分类,该论文利用(特权)权限集和应用的入口点来估算其攻击面。

风险类别 权限 保护级别
极高 android.permission.INSTALL_PACKAGES signature
关键 android.permission.COPY_PROTECTED_DATA signature
关键 android.permission.WRITE_SECURE_SETTINGS signature
关键 android.permission.READ_FRAME_BUFFER signature
关键 android.permission.MANAGE_CA_CERTIFICATES signature
关键 android.permission.MANAGE_APP_OPS_MODES signature
关键 android.permission.GRANT_RUNTIME_PERMISSIONS signature
关键 android.permission.DUMP signature
关键 android.permission.CAMERA dangerous
关键 android.permission.SYSTEM_CAMERA signatureOrSystem
关键 android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS signature
关键 android.permission.MOUNT_UNMOUNT_FILESYSTEMS signature
关键 android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE signature
关键 android.permission.PROVIDE_REMOTE_CREDENTIALS signature
关键 android.permission.THREAD_NETWORK_PRIVILEGED signature
关键 android.permission.RECORD_SENSITIVE_CONTENT signature
关键 android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS signature
android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS signature
android.permission.READ_SMS dangerous
android.permission.WRITE_SMS normal
android.permission.RECEIVE_MMS dangerous
android.permission.SEND_SMS_NO_CONFIRMATION signature
android.permission.RECEIVE_SMS dangerous
android.permission.READ_LOGS signature
android.permission.READ_PRIVILEGED_PHONE_STATE signature
android.permission.LOCATION_HARDWARE signature
android.permission.ACCESS_FINE_LOCATION dangerous
android.permission.ACCESS_BACKGROUND_LOCATION dangerous
android.permission.BIND_ACCESSIBILITY_SERVICE signature
android.permission.ACCESS_WIFI_STATE normal
com.android.voicemail.permission.READ_VOICEMAIL signature
android.permission.RECORD_AUDIO dangerous
android.permission.CAPTURE_AUDIO_OUTPUT signature
android.permission.ACCESS_NOTIFICATIONS signature
android.permission.INTERACT_ACROSS_USERS_FULL signature
android.permission.BLUETOOTH_PRIVILEGED signature
android.permission.GET_PASSWORD signature
android.permission.INTERNAL_SYSTEM_WINDOW signature
android.permission.MANAGE_ONGOING_CALLS signature
android.permission.READ_RESTRICTED_STATS internal
android.permission.BIND_AUTOFILL_SERVICE signature
android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS signature
android.permission.READ_DROPBOX_DATA signature
android.permission.WRITE_FLAGS signature
android.permission.ACCESS_COARSE_LOCATION dangerous
android.permission.CHANGE_COMPONENT_ENABLED_STATE signature
android.permission.READ_CONTACTS dangerous
android.permission.WRITE_CONTACTS dangerous
android.permission.CONNECTIVITY_INTERNAL signature
android.permission.ACCESS_MEDIA_LOCATION dangerous
android.permission.READ_EXTERNAL_STORAGE dangerous
android.permission.WRITE_EXTERNAL_STORAGE dangerous
android.permission.SYSTEM_ALERT_WINDOW signature
android.permission.READ_CALL_LOG dangerous
android.permission.WRITE_CALL_LOG dangerous
android.permission.INTERACT_ACROSS_USERS signature
android.permission.MANAGE_USERS signature
android.permission.READ_CALENDAR dangerous
android.permission.BLUETOOTH_ADMIN normal
android.permission.BODY_SENSORS dangerous
android.permission.MANAGE_EXTERNAL_STORAGE signature
android.permission.ACCESS_BLOBS_ACROSS_USERS signature
android.permission.BLUETOOTH_ADVERTISE dangerous
android.permission.READ_MEDIA_AUDIO dangerous
android.permission.READ_MEDIA_IMAGES dangerous
android.permission.READ_MEDIA_VIDEO dangerous
android.permission.REGISTER_NSD_OFFLOAD_ENGINE signature
android.permission.ACCESS_LAST_KNOWN_CELL_ID signature
android.permission.USE_COMPANION_TRANSPORTS signature
android.permission.DOWNLOAD_WITHOUT_NOTIFICATION normal
android.permission.PACKAGE_USAGE_STATS signature
android.permission.MASTER_CLEAR signature
android.permission.DELETE_PACKAGES normal
android.permission.GET_PACKAGE_SIZE normal
android.permission.BLUETOOTH normal
android.permission.DEVICE_POWER signature
android.permission.READ_PRECISE_PHONE_STATE signature
android.permission.LOG_FOREGROUND_RESOURCE_USE signature
android.permission.MANAGE_DEFAULT_APPLICATIONS signature
android.permission.MANAGE_FACE signature
android.permission.REPORT_USAGE_STATS signature
android.permission.MANAGE_DISPLAYS signature
android.permission.RESTRICT_DISPLAY_MODES signature
android.permission.ACCESS_HIDDEN_PROFILES_FULL signature
android.permission.GET_BACKGROUND_INSTALLED_PACKAGES signature
android.permission.ACCESS_NETWORK_STATE normal
android.permission.RECEIVE_BOOT_COMPLETED normal
android.permission.WAKE_LOCK normal
android.permission.FLASHLIGHT normal
android.permission.VIBRATE normal
android.permission.WRITE_MEDIA_STORAGE signature
android.permission.MODIFY_AUDIO_SETTINGS normal

请注意,此分类可能会随时间变化。该论文给我们提供了一个例子

在 Android 10 之前,READ_PHONE_STATE 权限会被归类为 HIGH,因为它保护了永久设备标识符(例如 IMEI/MEID、IMSI、SIM 和构建序列号)。然而,从 Android 10 开始,大部分可用于追踪的敏感信息已被移动、重构或重新划分为一个新的权限 READ_PRIVILEGED_PHONE_STATE,将新权限置于 HIGH 类别,但导致 READ_PHONE_STATE 权限移至 LOW。

各 API 级别下的权限变更

Android 8.0 (API 级别 26) 变更

以下变更影响所有在 Android 8.0 (API 级别 26) 上运行的应用,即使是那些面向较低 API 级别的应用也不例外。

  • 联系人提供器使用情况统计信息变更:当应用请求 READ_CONTACTS 权限时,联系人使用情况数据的查询将返回近似值而非精确值(自动完成 API 不受此变更影响)。

面向 Android 8.0 (API 级别 26) 或更高版本的应用受以下影响

  • 账户访问和发现性改进:应用不再仅通过授予 GET_ACCOUNTS 权限即可访问用户账户,除非认证器拥有该账户或用户授予该访问权限。
  • 新电话权限:以下权限(分类为危险权限)现在是 PHONE 权限组的一部分
    • ANSWER_PHONE_CALLS 权限允许程序化地接听来电(通过 acceptRingingCall)。
    • READ_PHONE_NUMBERS 权限授予对设备中存储的电话号码的读取访问权限。
  • 授予危险权限时的限制:危险权限被分类为权限组(例如,STORAGE 组包含 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE)。在 Android 8.0 (API 级别 26) 之前,请求组中一个权限就足以同时获得该组的所有权限。这从 Android 8.0 (API 级别 26) 开始发生变化:每当应用在运行时请求权限时,系统将仅授予该特定权限。但是,请注意,对该权限组中权限的所有后续请求将自动授予,而无需向用户显示权限对话框。请参阅 Android 开发者文档中的此示例

    假设一个应用在其清单中同时列出了 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE 权限,用户授予了它。如果应用面向 API 级别 25 或更低版本,系统也会同时授予 WRITE_EXTERNAL_STORAGE 权限,因为它属于同一个 STORAGE 权限组,并且也已在清单中注册。如果应用面向 Android 8.0 (API 级别 26),系统当时只授予 READ_EXTERNAL_STORAGE ;但是,如果应用稍后请求 WRITE_EXTERNAL_STORAGE,系统将立即授予该权限,而无需提示用户。

    您可以在 Android 开发者文档中查看权限组列表。为了使其更具混淆性,Google 还警告说,特定权限可能会在未来版本的 Android SDK 中从一个组移动到另一个组,因此,应用的逻辑不应依赖于这些权限组的结构。最佳实践是,在需要时显式请求每个权限。

Android 9 (API 级别 28) 变更

以下变更影响所有在 Android 9 上运行的应用,即使是那些面向低于 28 API 级别的应用也不例外。

  • 限制访问通话记录READ_CALL_LOGWRITE_CALL_LOGPROCESS_OUTGOING_CALLS(危险)权限已从 PHONE 组移至新的 CALL_LOG 权限组。这意味着即使拥有 PHONE 组的权限,也无法访问通话记录。
  • 限制访问电话号码:在 Android 9 (API 级别 28) 上运行的应用,如果想读取电话号码,需要 READ_CALL_LOG 权限。
  • 限制访问 Wi-Fi 位置和连接信息:SSID 和 BSSID 值无法检索(例如通过 WifiManager.getConnectionInfo),除非满足所有以下条件
    • ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION 权限。
    • ACCESS_WIFI_STATE 权限。
    • 位置服务已启用(在设置 -> 位置下)。

面向 Android 9 (API 级别 28) 或更高版本的应用受以下影响

  • 构建序列号弃用:设备的硬件序列号无法读取(例如通过 Build.getSerial),除非授予 READ_PHONE_STATE(危险)权限。

Android 10 (API 级别 29) 变更

Android 10 (API 级别 29) 引入了多项用户隐私增强功能。有关权限的变更影响所有在 Android 10 (API 级别 29) 上运行的应用,包括那些面向较低 API 级别的应用。

  • 限制位置访问:位置访问的新权限选项“仅在使用应用时”。
  • 默认分区存储:面向 Android 10 (API 级别 29) 的应用无需声明任何存储权限即可访问其在外部存储中应用特定目录中的文件,以及从媒体库创建的文件。
  • 限制访问屏幕内容READ_FRAME_BUFFERCAPTURE_VIDEO_OUTPUTCAPTURE_SECURE_VIDEO_OUTPUT 权限现在仅限签名访问,这可以防止无声地访问设备屏幕内容。
  • 面向旧版应用的用户可见权限检查:首次运行面向 Android 5.1 (API 级别 22) 或更低版本的应用时,将向用户显示权限屏幕,他们可以在其中撤销对特定旧版权限的访问(这些权限以前在安装时会自动授予)。

权限强制执行

Activity 权限强制执行

权限通过清单中 <activity> 标签内的 android:permission 属性应用。这些权限限制了哪些应用可以启动该 Activity。权限在 Context.startActivityActivity.startActivityForResult 期间检查。未持有所需权限将导致从调用中抛出 SecurityException

Service 权限强制执行

通过清单中 <service> 标签内的 android:permission 属性应用的权限限制了谁可以启动或绑定到关联的 Service。权限在 Context.startServiceContext.stopServiceContext.bindService 期间检查。未持有所需权限将导致从调用中抛出 SecurityException

广播权限强制执行

通过 <receiver> 标签内的 android:permission 属性应用的权限限制了向关联 BroadcastReceiver 发送广播的访问。在 Context.sendBroadcast 返回后,尝试将发送的广播传递给给定接收器时,会检查所持有的权限。未持有所需权限不会抛出异常,结果是广播未发送。

可以将权限提供给 Context.registerReceiver,以控制谁可以向程序化注册的接收器广播。反之,在调用 Context.sendBroadcast 时可以提供权限,以限制哪些广播接收器被允许接收广播。

请注意,接收器和广播器都可以要求权限。发生这种情况时,必须通过两个权限检查才能将 intent 传递到关联的目标。有关更多信息,请参考 Android 开发者文档中“使用权限限制广播”一节。

内容提供器权限强制执行

通过 <provider> 标签内的 android:permission 属性应用的权限限制了对 ContentProvider 中数据的访问。内容提供器有一个重要的附加安全设施,称为 URI 权限,接下来将对此进行描述。与其他组件不同,ContentProvider 有两个单独的权限属性可以设置:android:readPermission 限制谁可以从提供器读取,android:writePermission 限制谁可以写入。如果 ContentProvider 同时受读写权限保护,则仅持有写入权限并不会同时授予读取权限。

首次检索提供器以及使用 ContentProvider 执行操作时,都会检查权限。使用 ContentResolver.query 需要持有读取权限;使用 ContentResolver.insertContentResolver.updateContentResolver.delete 需要写入权限。在所有这些情况下,如果未持有适当的权限,调用将抛出 SecurityException

内容提供器 URI 权限

标准权限系统在与内容提供器一起使用时不够充分。例如,内容提供器可能希望将权限限制为读取权限以保护自身,同时使用自定义 URI 来检索信息。应用程序应仅拥有该特定 URI 的权限。

解决方案是按 URI 权限。从活动开始或返回结果时,方法可以设置 Intent.FLAG_GRANT_READ_URI_PERMISSION 和/或 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这会授予活动对特定 URI 的权限,无论其是否具有从内容提供器访问数据的权限。

这允许一种常见的基于能力的模型,其中用户交互驱动临时授予细粒度权限。这可以成为将应用所需权限减少到仅与其行为直接相关的关键设施。如果没有这种模型,恶意用户可能会通过未受保护的 URI 访问其他成员的电子邮件附件或收集联系人列表以备将来使用。在清单中,android:grantUriPermissions 属性或节点有助于限制 URI。

您可以在此处找到有关 URI 权限相关 API 的更多信息

自定义权限

Android 允许应用向其他应用公开其服务/组件。应用访问暴露的组件需要自定义权限。您可以在 AndroidManifest.xml 中通过创建具有两个强制属性 android:nameandroid:protectionLevel 的权限标签来定义自定义权限

创建符合最小权限原则的自定义权限至关重要:权限应明确定义其目的,并具有有意义且准确的标签和描述。

下面是一个名为 START_MAIN_ACTIVITY 的自定义权限示例,在启动 TEST_ACTIVITY Activity 时需要此权限。

第一个代码块定义了新的权限,它是自解释的。label 标签是权限的摘要,description 是摘要的更详细版本。您可以根据将授予的权限类型设置保护级别。定义权限后,可以通过将其添加到应用程序的清单中来强制执行它。在我们的示例中,第二个代码块表示我们将用创建的权限限制的组件。可以通过添加 android:permission 属性来强制执行。

<permission android:name="com.example.myapp.permission.START_MAIN_ACTIVITY"
        android:label="Start Activity in myapp"
        android:description="Allow the app to launch the activity of myapp app, any app you grant this permission will be able to launch main activity by myapp app."
        android:protectionLevel="normal" />

<activity android:name="TEST_ACTIVITY"
    android:permission="com.example.myapp.permission.START_MAIN_ACTIVITY">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
</activity>

创建权限 START_MAIN_ACTIVITY 后,应用可以通过 AndroidManifest.xml 文件中的 uses-permission 标签请求它。任何被授予自定义权限 START_MAIN_ACTIVITY 的应用都可以启动 TEST_ACTIVITY。请注意,<uses-permission android:name="myapp.permission.START_MAIN_ACTIVITY" /> 必须在 <application> 之前声明,否则在运行时会发生异常。请参阅下面基于权限概览清单介绍的示例。

<manifest>
<uses-permission android:name="com.example.myapp.permission.START_MAIN_ACTIVITY" />
        <application>
            <activity>
            </activity>
        </application>
</manifest>

我们建议在注册权限时使用反向域名注解,如上例所示(例如 com.domain.application.permission),以避免与其他应用程序冲突。

WebViews

WebViews 中的 URL 加载

WebViews 是 Android 的嵌入式组件,允许您的应用在其内部打开网页。除了与移动应用相关的威胁之外,WebViews 还可能使您的应用面临常见的网络威胁(例如 XSS、开放重定向等)。

测试 WebView 时最重要的事情之一是确保其中只加载受信任的内容。任何新加载的页面都可能具有恶意性,试图利用任何 WebView 绑定或试图对用户进行钓鱼。除非您正在开发浏览器应用,否则通常会希望将加载的页面限制在您应用的域内。一个好的做法是防止用户甚至有机会在 WebView 中输入任何 URL(这是 Android 上的默认设置),也防止导航到受信任域之外。即使在受信任域中导航,用户仍然可能遇到并点击指向不可信内容的链接(例如,如果页面允许其他用户发布评论)。此外,一些开发者甚至可能覆盖一些默认行为,这可能对用户造成潜在危险。

安全浏览 API

为了提供更安全的网页浏览体验,Android 8.1 (API 级别 27) 引入了 SafeBrowsing API,它允许您的应用检测 Google 已归类为已知威胁的 URL。

默认情况下,WebView 会向用户显示关于安全风险的警告,并提供加载 URL 或停止页面加载的选项。通过 SafeBrowsing API,您可以自定义应用的行为,要么向 SafeBrowsing 报告威胁,要么在每次遇到已知威胁时执行特定操作(例如返回安全页面)。请查阅Android 开发者文档以获取使用示例。

您可以利用SafetyNet 库独立于 WebView 使用 SafeBrowsing API,该库实现了 Safe Browsing 网络协议 v4 的客户端。SafetyNet 允许您分析应用应该加载的所有 URL。由于 SafeBrowsing 与 URL 方案无关,您可以检查具有不同方案(例如 http、file)的 URL,并针对 TYPE_POTENTIALLY_HARMFUL_APPLICATIONTYPE_SOCIAL_ENGINEERING 威胁类型进行检查。

在发送 URL 或文件以检查已知威胁时,请确保它们不包含可能损害用户隐私或暴露应用敏感内容的敏感数据。

VirusTotal API

VirusTotal 提供了一个 API,用于分析 URL 和本地文件中的已知威胁。API 参考可在 VirusTotal 开发者页面上找到。

WebViews 中的 JavaScript 执行

JavaScript 可以通过反射型、存储型或基于 DOM 的跨站脚本 (XSS) 注入到 Web 应用程序中。移动应用在沙箱环境中执行,并且在原生实现时没有此漏洞。然而,WebView 可能是原生应用的一部分,以允许网页浏览。每个应用都有自己的 WebView 缓存,不与原生浏览器或其他应用共享。

在 Android 4.4 之前的版本中,WebView 使用 WebKit 渲染引擎显示网页。自 Android 4.4 起,WebView 已基于 Chromium,提供了改进的性能和兼容性。然而,页面仍被精简为最少功能;例如,页面没有地址栏。

Android WebView 可以使用 setJavaScriptEnabled 来启用 JavaScript 执行。此功能默认禁用,但如果启用,则可用于在加载页面的上下文中执行 JavaScript 代码。如果 WebView 加载了不受信任的内容,这可能很危险,因为它可能导致 XSS 攻击。如果您需要启用 JavaScript,请确保内容是受信任的,并且已实施正确的输入验证和输出编码。否则,您可以显式禁用 JavaScript

webView.settings.apply {
    javaScriptEnabled = false
}

WebView 本地文件访问设置

这些 API 控制 WebView 如何访问本地设备上的文件。它们决定 WebView 是否可以从文件系统加载文件(例如 HTML、图像或脚本),以及在本地上下文中运行的 JavaScript 是否可以访问额外的本地文件。请注意,无论这些设置如何,访问资产和资源(通过 file:///android_asset 或 file:///android_res)始终是允许的。

API 目的 默认为 True (API 级别) 默认为 False (API 级别) 已弃用
setAllowFileAccess 允许 WebView 从本地文件系统加载文件(使用 file:// URL) <= 29 (Android 10) >= 30 (Android 11)
setAllowFileAccessFromFileURLs 允许 file:// 上下文中的 JavaScript 访问其他本地 file:// URL <= 15 (Android 4.0.3) >= 16 (Android 4.1) 是(从 API 级别 30,Android 11 开始)
setAllowUniversalAccessFromFileURLs 允许 file:// 上下文中的 JavaScript 访问来自任何源的资源,绕过同源策略 <= 15 (Android 4.0.3) >= 16 (Android 4.1) 是(从 API 级别 30,Android 11 开始)

WebView 可以访问哪些文件?

WebView 可以通过 file:// URL 访问应用有权访问的任何文件,包括

  • 内部存储:应用自身的内部存储。
  • 外部存储
    • Android 10 之前
      • 整个外部存储(SD 卡),如果应用拥有 READ_EXTERNAL_STORAGE 权限。
    • Android 10 之后
      • 只有应用特定目录(由于分区存储限制),无需任何特殊权限。
      • 整个媒体文件夹(包括来自其他应用的数据),如果应用拥有 READ_MEDIA_IMAGES 或类似权限。
      • 整个外部存储,如果应用拥有 MANAGE_EXTERNAL_STORAGE 权限。
setAllowFileAccess

setAllowFileAccess 启用 WebView 使用 file:// 方案加载本地文件。在此示例中,WebView 配置为允许文件访问,然后从外部存储(sdcard)加载 HTML 文件。

webView.settings.apply {
    allowFileAccess = true
}
webView.loadUrl("file:///sdcard/index.html");
setAllowFileAccessFromFileURLs

setAllowFileAccessFromFileURLs 允许本地文件(通过 file:// 加载)从其 HTML 或 JavaScript 访问额外的本地资源。

请注意,如果 allowUniversalAccessFromFileURLs 的值为 true,则 此设置的值将被忽略

Chromium WebView 文档:通过此宽松的来源规则,以 content://file:// 开头的 URL 可以通过 XMLHttpRequest 访问具有相同宽松来源的资源。例如,file://foo 可以向 file://bar 发出 XMLHttpRequest 请求。开发人员需要小心,不要让用户提供的数据在 content:// 中运行,因为它将允许用户的代码访问其他应用程序提供的任意 content:// URL。这会导致严重的安全问题。

无论此 API 调用如何,Fetch API 都不允许访问 content://file:// URL。

示例:在此示例中,WebView 配置为允许文件访问,然后从外部存储(sdcard)加载 HTML 文件。

webView.settings.apply {
    allowFileAccess = true
    allowFileAccessFromFileURLs = true
}
webView.loadUrl("file:///sdcard/local_page.html");

加载的 HTML 文件包含通过 file:// URL 加载的图像

<!-- In local_page.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Local Page</title>
  </head>
  <body>
    <!-- This image is loaded via a file:// URL -->
    <img src="file:///android_asset/images/logo.png" alt="Logo">
  </body>
</html>
setAllowUniversalAccessFromFileURLs

setAllowUniversalAccessFromFileURLs 允许在本地文件(通过 file:// 加载)中运行的 JavaScript 绕过同源策略并访问来自任何来源的资源。

Chromium WebView 文档:当此 API 以 true 调用时,以 file:// 开头的 URL 将具有基于方案的源,并且可以通过 XMLHttpRequest 访问其他基于方案的 URL。例如,file://foo 可以向 content://barhttp://example.com/https://www.google.com/ 发出 XMLHttpRequest 请求。因此,开发者需要管理在 file:// 方案下运行的数据,因为它允许超越公共 Web CORS 策略的强大权限。

无论此 API 调用如何,Fetch API 都不允许访问 content://file:// URL。

示例:在此示例中,本地 HTML 文件成功发起跨域请求以从 HTTPS 端点获取数据。攻击者可能会滥用此功能,从应用中窃取敏感数据。

webView.settings.apply {
    javaScriptEnabled = true
    allowFileAccess = true
    allowUniversalAccessFromFileURLs = true
}
webView.loadUrl("file:///android_asset/local_page.html");

local_page.html 的内容(在 assets 文件夹中)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Universal Access Demo</title>
    <script>
      // This AJAX call fetches data from a remote server despite being loaded via file://
      fetch("https://api.example.com/data")
        .then(response => response.text())
        .then(data => document.getElementById("output").innerText = data)
        .catch(err => console.error(err));
    </script>
  </head>
  <body>
    <div id="output">Loading...</div>
  </body>
</html>

关于访问 Cookie 的注意事项

setAllowUniversalAccessFromFileURLs(true) 设置为 true 允许本地 file:// 中的 JavaScript 发出跨域请求(XHR、Fetch 等)。这绕过了网络请求的同源策略 (SOP),但它不授予对远程网站 Cookie 的访问权限。

  • Cookie 由 WebView 的 CookieManager 管理,除非通过 document.cookie 明确允许(大多数现代网站使用 HttpOnlySecure 标志来阻止),否则 file:// 源无法访问它们。
  • 跨域请求也不包含 Cookie,除非服务器通过 CORS 标头明确允许,例如 Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true

WebView 内容提供器访问

WebView 可以访问内容提供器,内容提供器用于在应用程序之间共享数据。内容提供器只有在被导出(android:exported 属性设置为 true)时才能被其他应用访问,但即使内容提供器未导出,它也可以被应用程序本身的 WebView 访问。

设置 setAllowContentAccess 控制 WebView 是否可以使用 content:// URL 访问内容提供器。此设置在 Android 4.1 (API 级别 16) 及更高版本中默认启用。

Chromium WebView 文档content:// URL 用于通过 Android 内容提供器访问资源。访问应事先通过 setAllowContentAccess API 允许。content:// 页面可以包含加载 content:// 页面的 iframe,但父框架无法访问 iframe 内容。此外,只有 content:// 页面可以为子资源指定 content:// URL。

然而,即使是从 content:// 加载的页面也无法向其他 content:// URL 发出任何支持 CORS 的请求(例如 XMLHttpRequest),因为每个 URL 都被假定属于一个不透明源。另请参阅 setAllowFileAccessFromFileURLssetAllowUniversalAccessFromFileURLs 部分,因为它们可以放宽此默认规则。

使用除 content:// 之外的任何方案加载的页面都无法在 iframe 中加载 content:// 页面,也无法为子资源指定 content:// URL。

示例:在此示例中,WebView 的 allowContentAccess 属性已启用,并加载了一个 content:// URL。

webView.settings.apply {
    allowContentAccess = true
}
webView.loadUrl("content://com.example.myapp.provider/data");

WebView 可以访问哪些文件?

WebView 可以使用 content:// URL 访问通过内容提供器可访问的任何数据(如果应用有)。除非内容提供器另有进一步限制,这可能包括

  • 内部存储:整个内部存储。
  • 外部存储
    • Android 10 之前
      • 整个外部存储(SD 卡),如果应用拥有 READ_EXTERNAL_STORAGE 权限。
    • Android 10 之后
      • 只有应用特定目录(由于分区存储限制),无需任何特殊权限。
      • 整个媒体文件夹(包括来自其他应用的数据),如果应用拥有 READ_MEDIA_IMAGES 或类似权限。
      • 整个外部存储,如果应用拥有 MANAGE_EXTERNAL_STORAGE 权限。

通过内容提供器可访问的其他应用数据(如果应用有且已导出)也可以访问。

通过 WebViews 暴露的 Java 对象

Android 提供了一种在 WebView 中执行 JavaScript 的方式,通过使用 addJavascriptInterface 方法调用和使用 Android 应用的原生函数(用 @JavascriptInterface 注解)。这被称为 WebView JavaScript 桥原生桥

请注意,当您使用 addJavascriptInterface 时,您是明确地授予该 WebView 中加载的所有页面访问已注册的 JavaScript 接口对象的权限。这意味着,如果用户导航到您的应用或域之外,所有其他外部页面也将能够访问这些 JavaScript 接口对象,如果通过这些接口暴露任何敏感数据,这可能会带来潜在的安全风险。

警告:对于面向 Android 4.2 (API 级别 17) 以下 Android 版本的应用要格外小心,因为它们在 addJavascriptInterface 的实现中存在漏洞:一种滥用反射的攻击,当恶意 JavaScript 注入到 WebView 中时,会导致远程代码执行。这是因为所有 Java 对象方法默认都是可访问的(而不是只有那些带有注解的方法)。

WebViews 清理

当应用访问 WebView 内的任何敏感数据时,清除 WebView 资源是关键一步。这包括任何本地存储的文件、RAM 缓存和任何加载的 JavaScript。

作为附加措施,您可以使用服务器端标头,例如 no-cache,这可以防止应用程序缓存特定内容。

从 Android 10 (API 级别 29) 开始,应用能够检测 WebView 是否变得无响应。如果发生这种情况,操作系统将自动调用 onRenderProcessUnresponsive 方法。

您可以在Android 开发者网站上找到更多使用 WebView 的安全最佳实践。

深层链接是任意方案的 URI,可将用户直接带到应用中的特定内容。应用可以通过在 Android Manifest 中添加意图过滤器并从传入的意图中提取数据来设置深层链接,以将用户导航到正确的活动。

Android 支持两种类型的深层链接

  • 自定义 URL 方案,即使用任何自定义 URL 方案的深层链接,例如 myapp://(未经验证操作系统)。
  • Android 应用链接(Android 6.0 (API 级别 23) 及更高版本),即使用 http://https:// 方案并包含 autoVerify 属性(触发操作系统验证)的深层链接。

深层链接冲突

使用未经验证的深层链接可能会导致一个重大问题——用户设备上安装的任何其他应用都可以声明并尝试处理相同的意图,这被称为深层链接冲突。任何任意应用程序都可以声明控制与另一个应用程序完全相同的深层链接。

在最新版本的 Android 中,这会导致向用户显示一个所谓的消歧对话框,要求他们选择应处理深层链接的应用程序。用户可能会错误地选择恶意应用程序而不是合法的应用程序。

Android 应用链接

为了解决深层链接冲突问题,Android 6.0 (API 级别 23) 引入了 Android 应用链接,它们是基于开发者显式注册的网站 URL 的已验证深层链接。点击应用链接后,如果应用已安装,将立即打开该应用。

与未经验证的深层链接有一些关键区别

  • 应用链接仅使用 http://https:// 方案,不允许任何其他自定义 URL 方案。
  • 应用链接需要一个活跃的域名通过 HTTPS 提供数字资产链接文件
  • 应用链接不会出现深层链接冲突,因为当用户打开它们时,不会显示消歧对话框。

通过 IPC 暴露敏感功能

在移动应用开发过程中,开发者可能会采用传统的 IPC 技术(例如使用共享文件或网络套接字)。应使用移动应用平台提供的 IPC 系统功能,因为它比传统技术成熟得多。使用 IPC 机制而不考虑安全性可能会导致应用泄露或暴露敏感数据。

以下是可能暴露敏感数据的 Android IPC 机制列表

待定 Intent

在应用开发过程中处理复杂流程时,经常会出现应用 A 希望应用 B 在未来代表应用 A 执行特定操作的情况。如果仅使用 Intent 来实现这一点,会导致各种安全问题,例如存在多个导出的组件。为了以安全的方式处理这种情况,Android 提供了 PendingIntent API。

PendingIntent 最常用于通知应用小部件媒体浏览器服务等。当用于通知时,PendingIntent 用于声明当用户对应用的通知执行操作时要执行的 intent。通知需要一个回调到应用,以便在用户点击时触发一个操作。

在内部,PendingIntent 对象封装了一个普通的 Intent 对象(称为基本 intent),该对象最终将用于调用一个操作。例如,基本 intent 指定应在应用程序中启动活动 A。接收 PendingIntent 的应用程序将解封并检索此基本 intent,并通过调用 PendingIntent.send 函数来调用活动 A。

使用 PendingIntent 的典型实现如下

Intent intent = new Intent(applicationContext, SomeActivity.class);     // base intent

// create a pending intent
PendingIntent pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);

// send the pending intent to another app
Intent anotherIntent = new Intent();
anotherIntent.setClassName("other.app", "other.app.MainActivity");
anotherIntent.putExtra("pendingIntent", pendingIntent);
startActivity(anotherIntent);

PendingIntent 安全的原因在于,与普通 Intent 不同,它授予外部应用程序使用其包含的 Intent(基本 intent)的权限,就好像它是由您自己的应用程序进程执行的一样。这允许应用程序自由使用它们来创建回调,而无需创建导出的 Activity。

如果实现不正确,恶意应用程序可以劫持一个 PendingIntent。例如,在上面的通知示例中,具有 android.permission.BIND_NOTIFICATION_LISTENER_SERVICE 权限的恶意应用程序可以绑定到通知侦听器服务并检索待定 intent。

在实现 PendingIntent 时存在某些安全陷阱,如下所示

  • 可变字段PendingIntent 可以包含可变且为空的字段,这些字段可以被恶意应用程序填充。这可能导致恶意应用程序获得对未导出应用程序组件的访问权限。使用 PendingIntent.FLAG_IMMUTABLE 标志可以使 PendingIntent 不可变,并防止对字段进行任何更改。在 Android 12 (API 级别 31) 之前,PendingIntent 默认是可变的,而从 Android 12 (API 级别 31) 开始,它已更改为默认不可变,以防止意外的漏洞。

  • 使用隐式 intent:恶意应用程序可以接收一个 PendingIntent,然后更新基本 intent 以将目标指向恶意应用程序内的组件和包。作为缓解措施,请确保您明确指定将接收基本 intent 的确切包、动作和组件。

PendingIntent 攻击最常见的情况是恶意应用程序能够拦截它。

有关更多详细信息,请查看 Android 文档中关于使用待定 intent 的内容。

隐式 Intent

Intent 是一种消息对象,您可以使用它来请求另一个应用程序组件执行操作。尽管 Intent 以多种方式促进组件之间的通信,但有三个基本用例:启动 Activity、启动 Service 和传递广播。

根据Android 开发者文档,Android 提供了两种类型的 Intent

  • 显式 Intent 通过提供目标应用的包名或完全限定的组件类名来指定哪个应用程序将满足该 Intent。通常,您会使用显式 Intent 在自己的应用中启动组件,因为您知道要启动的 Activity 或 Service 的类名。例如,您可能希望在响应用户操作时在应用中启动新的 Activity,或者启动服务以在后台下载文件。
// Note the specification of a concrete component (DownloadActivity) that is started by the intent.
Intent downloadIntent = new Intent(this, DownloadActivity.class);
downloadIntent.setAction("android.intent.action.GET_CONTENT")
startActivityForResult(downloadIntent);
  • 隐式 Intent 不指定特定组件,而是声明一个要执行的通用操作,该操作可以由另一个应用的组件处理。例如,如果您想在地图上向用户显示某个位置,您可以使用隐式 Intent 请求另一个有能力的应用在地图上显示特定位置。另一个例子是当用户在应用中点击电子邮件地址时,调用应用不想指定特定的电子邮件应用,而将选择权留给用户。
// Developers can also start an activity by just setting an action that is matched by the intended app.
Intent downloadIntent = new Intent();
downloadIntent.setAction("android.intent.action.GET_CONTENT")
startActivityForResult(downloadIntent);

使用隐式 Intent 可能会导致多个安全风险,例如,如果调用应用在没有适当验证的情况下处理隐式 Intent 的返回值,或者如果 Intent 包含敏感数据,则可能会意外泄露给未经授权的第三方。

您可以参考这篇博客文章这篇文章CWE-927,了解有关所述问题、具体攻击场景和建议的更多信息。

对象持久化

在 Android 上持久化对象有几种方法

对象序列化

对象及其数据可以表示为字节序列。这在 Java 中通过对象序列化完成。序列化本身并不安全。它只是用于将数据本地存储在 .ser 文件中的二进制格式(或表示)。只要密钥安全存储,就可以加密和签名 HMAC 序列化数据。反序列化对象需要与用于序列化对象的类相同版本的类。类更改后,ObjectInputStream 无法从旧的 .ser 文件创建对象。下面的示例展示了如何通过实现 Serializable 接口来创建 Serializable 类。

import java.io.Serializable;

public class Person implements Serializable {
  private String firstName;
  private String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
  //..
  //getters, setters, etc
  //..

}

现在,您可以在另一个类中使用 ObjectInputStream/ObjectOutputStream 读取/写入对象。

JSON

有几种方法可以将对象的内容序列化为 JSON。Android 附带 JSONObjectJSONArray 类。各种库,包括 GSONJacksonMoshi 也可以使用。这些库之间的主要区别在于它们是否使用反射来组合对象、是否支持注解、是否创建不可变对象以及它们使用的内存量。请注意,几乎所有的 JSON 表示都是基于字符串的,因此是不可变的。这意味着存储在 JSON 中的任何秘密都将更难从内存中删除。JSON 本身可以存储在任何地方,例如(NoSQL)数据库或文件。您只需要确保包含秘密的任何 JSON 都已得到适当的保护(例如,加密/HMACed)。有关更多详细信息,请参阅“Android 上的数据存储”一章。下面是一个使用 GSON 写入和读取 JSON 的简单示例(来自 GSON 用户指南)。在此示例中,BagOfPrimitives 实例的内容被序列化为 JSON

class BagOfPrimitives {
  private int value1 = 1;
  private String value2 = "abc";
  private transient int value3 = 3;
  BagOfPrimitives() {
    // no-args constructor
  }
}

// Serialization
BagOfPrimitives obj = new BagOfPrimitives();
Gson gson = new Gson();
String json = gson.toJson(obj);

// ==> json is {"value1":1,"value2":"abc"}

XML

有几种方法可以将对象的内容序列化为 XML 并反序列化。Android 附带 XmlPullParser 接口,它允许轻松维护 XML 解析。Android 中有两种实现:KXmlParserExpatPullParserAndroid 开发者指南提供了关于如何使用它们的精彩介绍。此外,还有各种替代方案,例如 Java 运行时附带的 SAX 解析器。有关更多信息,请参阅ibm.com 上的一篇博客文章。与 JSON 类似,XML 也存在主要基于字符串的问题,这意味着字符串类型的秘密将更难从内存中移除。XML 数据可以存储在任何地方(数据库、文件),但在涉及秘密或不应更改的信息时确实需要额外保护。有关更多详细信息,请参阅“Android 上的数据存储”一章。如前所述:XML 的真正危险在于 XML 外部实体 (XXE) 攻击,因为它可能允许读取应用程序内仍然可访问的外部数据源。

ORM

有一些库提供将对象内容直接存储到数据库中,然后用数据库内容实例化对象的功能。这称为对象关系映射 (ORM)。使用 SQLite 数据库的库包括

另一方面,Realm 使用自己的数据库存储类的内容。ORM 可以提供的保护量主要取决于数据库是否加密。有关更多详细信息,请参阅“Android 上的数据存储”一章。Realm 网站包含一个不错的 ORM Lite 示例

Parcelable

Parcelable 是一个接口,用于其实例可以写入 Parcel 并从中恢复的类。Parcel 通常用于将类打包为 IntentBundle 的一部分。以下是一个实现 Parcelable 的 Android 开发者文档示例

public class MyParcelable implements Parcelable {
     private int mData;

     public int describeContents() {
         return 0;
     }

     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(mData);
     }

     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<MyParcelable>() {
         public MyParcelable createFromParcel(Parcel in) {
             return new MyParcelable(in);
         }

         public MyParcelable[] newArray(int size) {
             return new MyParcelable[size];
         }
     };

     private MyParcelable(Parcel in) {
         mData = in.readInt();
     }
 }

由于涉及 Parcel 和 Intent 的这种机制可能会随时间变化,并且 Parcelable 可能包含 IBinder 指针,因此不建议通过 Parcelable 将数据存储到磁盘。

Protocol Buffers

Google 的Protocol Buffers是一种平台和语言无关的机制,通过二进制数据格式序列化结构化数据。Protocol Buffers 存在一些漏洞,例如 CVE-2015-5237。请注意,Protocol Buffers 不提供任何保密保护:没有内置加密。

覆盖攻击

屏幕覆盖攻击发生在恶意应用程序设法将自己置于另一个应用程序之上,而该应用程序仍像在前景中一样正常工作。恶意应用程序可能会创建模仿原始应用程序甚至 Android 系统 UI 外观和感觉的 UI 元素。其目的通常是让用户相信他们仍在与合法应用程序交互,然后试图提升权限(例如通过获得某些权限)、隐秘钓鱼、捕获用户点击和击键等。

有几种影响不同 Android 版本的攻击,包括

  • 点击劫持(Android 6.0 (API 级别 23) 及更低版本)滥用 Android 的屏幕覆盖功能,侦听点击并拦截传递给底层 Activity 的任何信息。
  • “斗篷与匕首”攻击影响面向 Android 5.0 (API 级别 21) 至 Android 7.1 (API 级别 25) 的应用。它们滥用 SYSTEM_ALERT_WINDOW (“在顶部绘制”) 和 BIND_ACCESSIBILITY_SERVICE (“辅助功能”) 权限中的一个或两个,如果应用是从 Play 商店安装的,用户无需明确授予这些权限,甚至不会收到通知。
  • Toast 叠加与“斗篷与匕首”非常相似,但不需要用户授予特定的 Android 权限。它在 Android 8.0 (API 级别 26) 上通过 CVE-2017-0752 修复。

通常,这类攻击是 Android 系统版本固有的某些漏洞或设计问题。这使得它们难以防范,并且通常几乎不可能防止,除非将应用升级到安全的 Android 版本(API 级别)。

多年来,许多已知恶意软件,如 MazorBot、BankBot 或 MysteryBot,一直在滥用 Android 的屏幕叠加功能来攻击业务关键型应用程序,特别是在银行业。这篇博客讨论了更多关于这类恶意软件的信息。

强制更新

从 Android 5.0 (API 级别 21) 开始,结合 Play Core Library,可以强制应用进行更新。此机制基于使用 AppUpdateManager。在此之前,还使用了其他机制,例如向 Google Play 商店发出 HTTP 调用,但由于 Play 商店的 API 可能会更改,这些机制的可靠性不如前者。另外,也可以使用 Firebase 来检查可能的强制更新(参见这篇博客)。强制更新在公钥锁定(详见“测试网络通信”一节)方面非常有用,当由于证书/公钥轮换而需要刷新锁定时。此外,通过强制更新可以轻松修补漏洞。

请注意,应用程序的较新版本不会修复应用程序与之通信的后端中存在的安全问题。仅仅允许应用程序不与后端通信可能还不够。适当的 API 生命周期管理是关键。同样,当用户未被迫更新时,请不要忘记根据您的 API 测试旧版应用程序,和/或使用适当的 API 版本控制。