iOS 平台 API¶
概述¶
强制更新¶
当由于证书/公钥轮换需要刷新 pin 时,强制更新在公钥固定(详情请参见 测试网络通信)方面可能会有所帮助。此外,可以通过强制更新轻松修补漏洞。
然而,iOS 面临的挑战是,Apple 尚未提供任何 API 来自动化此过程,相反,开发人员必须创建自己的机制,如各种 博客 中描述的那样,这些机制可以归结为使用 http://itunes.apple.com/lookup\?id\<BundleId>
或第三方库(例如 Siren 和 react-native-appstore-version-checker)查找 App 的属性。大多数这些实现都需要 API 提供的某个给定版本或仅需要“应用商店中的最新版本”,这意味着即使实际上没有业务/安全需要更新,用户也可能会对必须更新 App 感到沮丧。
请注意,应用程序的较新版本不会修复应用程序与之通信的后端中存在的安全问题。允许应用程序不与其通信可能是不够的。拥有适当的 API 生命周期管理是关键。同样,当用户未被强制更新时,请不要忘记针对您的 API 测试您的应用程序的旧版本和/或使用适当的 API 版本控制。
对象持久化¶
有多种方法可以在 iOS 上持久化对象
对象编码¶
iOS 带有两种用于 Objective-C 或 NSObject
的对象编码和解码协议:NSCoding
和 NSSecureCoding
。当一个类符合其中任何一个协议时,数据将序列化为 NSData
:字节缓冲区的包装器。请注意,Swift 中的 Data
与 NSData
或其可变对应物 NSMutableData
相同。NSCoding
协议声明了为了编码/解码其实例变量必须实现的两个方法。使用 NSCoding
的类需要实现 NSObject
或被注释为 @objc 类。NSCoding
协议要求实现 encode 和 init,如下所示。
class CustomPoint: NSObject, NSCoding {
//required by NSCoding:
func encode(with aCoder: NSCoder) {
aCoder.encode(x, forKey: "x")
aCoder.encode(name, forKey: "name")
}
var x: Double = 0.0
var name: String = ""
init(x: Double, name: String) {
self.x = x
self.name = name
}
// required by NSCoding: initialize members using a decoder.
required convenience init?(coder aDecoder: NSCoder) {
guard let name = aDecoder.decodeObject(forKey: "name") as? String
else {return nil}
self.init(x:aDecoder.decodeDouble(forKey:"x"),
name:name)
}
//getters/setters/etc.
}
NSCoding
的问题在于对象通常在您可以评估类类型之前就已经构建和插入。这使得攻击者可以轻松注入各种数据。因此,引入了 NSSecureCoding
协议。当符合 NSSecureCoding
时,您需要包含
static var supportsSecureCoding: Bool {
return true
}
当 init(coder:)
是该类的一部分时。接下来,当解码对象时,应该进行检查,例如
let obj = decoder.decodeObject(of:MyClass.self, forKey: "myKey")
符合 NSSecureCoding
可确保实例化的对象确实是预期的对象。但是,不会对数据进行额外的完整性检查,并且数据不会被加密。因此,任何秘密数据都需要额外的加密,并且必须保护其完整性的数据应该获得额外的 HMAC。
请注意,当使用 NSData
(Objective-C) 或关键字 let
(Swift) 时:那么数据在内存中是不可变的,并且不容易删除。
使用 NSKeyedArchiver 进行对象归档¶
NSKeyedArchiver
是 NSCoder
的具体子类,它提供了一种编码对象并将其存储在文件中的方法。NSKeyedUnarchiver
解码数据并重新创建原始数据。让我们以 NSCoding
部分的示例为例,现在进行归档和取消归档
// archiving:
NSKeyedArchiver.archiveRootObject(customPoint, toFile: "/path/to/archive")
// unarchiving:
guard let customPoint = NSKeyedUnarchiver.unarchiveObjectWithFile("/path/to/archive") as?
CustomPoint else { return nil }
当解码 keyed 归档时,由于值是按名称请求的,因此值可以按顺序解码或完全不解码。因此,Keyed 归档为向前和向后兼容性提供了更好的支持。这意味着磁盘上的归档实际上可能包含程序未检测到的额外数据,除非在稍后阶段提供该给定数据的密钥。
请注意,需要额外的保护措施来保护文件,以防涉及机密数据,因为数据未在文件中加密。有关更多详细信息,请参见 “iOS 上的数据存储”一章。
Codable¶
使用 Swift 4,Codable
类型别名到达:它是 Decodable
和 Encodable
协议的组合。String
、Int
、Double
、Date
、Data
和 URL
本质上是 Codable
:这意味着它们可以轻松地编码和解码,而无需任何额外的工作。让我们以下面的示例为例
struct CustomPointStruct:Codable {
var x: Double
var name: String
}
通过将 Codable
添加到示例中 CustomPointStruct
的继承列表中,将自动支持方法 init(from:)
和 encode(to:)
。有关 Codable
工作原理的更多详细信息,请查看 Apple Developer Documentation。可以使用 NSCoding
/NSSecureCoding
、JSON、属性列表、XML 等将 Codable
轻松编码/解码为各种表示形式。有关更多详细信息,请参见下面的子部分。
JSON 和 Codable¶
通过使用不同的第三方库,有多种方法可以在 iOS 中编码和解码 JSON
- Mantle
- JSONModel 库
- SwiftyJSON 库
- ObjectMapper 库
- JSONKit
- JSONModel
- YYModel
- SBJson 5
- Unbox
- Gloss
- Mapper
- JASON
- Arrow
这些库在对 Swift 和 Objective-C 的特定版本的支持、它们是否返回(不可)变结果、速度、内存消耗和实际库大小方面有所不同。同样,请注意在不可变的情况下:机密信息不容易从内存中删除。
接下来,Apple 通过将 Codable
与 JSONEncoder
和 JSONDecoder
结合使用,直接提供对 JSON 编码/解码的支持
struct CustomPointStruct: Codable {
var point: Double
var name: String
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let test = CustomPointStruct(point: 10, name: "test")
let data = try encoder.encode(test)
let stringData = String(data: data, encoding: .utf8)
// stringData = Optional ({
// "point" : 10,
// "name" : "test"
// })
JSON 本身可以存储在任何地方,例如,(NoSQL)数据库或文件。您只需要确保包含机密的任何 JSON 都已得到适当的保护(例如,加密/HMAC)。有关更多详细信息,请参见 “iOS 上的数据存储”一章。
属性列表和 Codable¶
您可以将对象持久化到属性列表(也称为 plists 在前面的部分中)。您可以在下面找到两个如何使用它的示例
// archiving:
let data = NSKeyedArchiver.archivedDataWithRootObject(customPoint)
NSUserDefaults.standardUserDefaults().setObject(data, forKey: "customPoint")
// unarchiving:
if let data = NSUserDefaults.standardUserDefaults().objectForKey("customPoint") as? NSData {
let customPoint = NSKeyedUnarchiver.unarchiveObjectWithData(data)
}
在第一个示例中,使用了 NSUserDefaults
,这是主要的属性列表。我们可以对 Codable
版本做同样的事情
struct CustomPointStruct: Codable {
var point: Double
var name: String
}
var points: [CustomPointStruct] = [
CustomPointStruct(point: 1, name: "test"),
CustomPointStruct(point: 2, name: "test"),
CustomPointStruct(point: 3, name: "test"),
]
UserDefaults.standard.set(try? PropertyListEncoder().encode(points), forKey: "points")
if let data = UserDefaults.standard.value(forKey: "points") as? Data {
let points2 = try? PropertyListDecoder().decode([CustomPointStruct].self, from: data)
}
请注意,plist
文件并非用于存储机密信息。它们旨在保存应用程序的用户首选项。
XML¶
有多种方法可以进行 XML 编码。与 JSON 解析类似,有各种第三方库,例如
它们在速度、内存使用率、对象持久性方面各不相同,更重要的是:它们在处理 XML 外部实体的方式上有所不同。请参见 Apple iOS Office 查看器中的 XXE 作为示例。因此,如果可能,禁用外部实体解析是关键。有关更多详细信息,请参见 OWASP XXE prevention cheatsheet。除了库之外,您还可以使用 Apple 的 XMLParser
类
当不使用第三方库,而是 Apple 的 XMLParser
时,请确保让 shouldResolveExternalEntities
返回 false
。
对象关系映射(CoreData 和 Realm)¶
iOS 有各种类似 ORM 的解决方案。第一个是 Realm,它带有自己的存储引擎。Realm 具有加密数据的设置,如 Realm 的文档中所述。这允许处理安全数据。请注意,默认情况下会关闭加密。
Apple 本身提供了 CoreData
,这在 Apple Developer Documentation 中得到了很好的解释。它支持各种存储后端,如 Apple 的持久存储类型和行为文档中所述。Apple 推荐的存储后端的问题在于,没有一种数据存储类型是加密的,也没有检查完整性。因此,在涉及机密数据的情况下,需要采取额外的措施。一种替代方案可以在 project iMas 中找到,它确实提供了开箱即用的加密。
协议缓冲区¶
Google 的协议缓冲区是一种平台和语言中立的机制,用于通过 二进制数据格式序列化结构化数据。它们可以通过 Protobuf 库用于 iOS。协议缓冲区存在一些漏洞,例如 CVE-2015-5237。请注意,协议缓冲区不提供任何机密性保护,因为没有可用的内置加密。
WebView¶
WebView 是用于显示交互式 Web 内容的应用内浏览器组件。它们可用于将 Web 内容直接嵌入到应用程序的用户界面中。iOS WebView 默认支持 JavaScript 执行,因此脚本注入和跨站点脚本攻击可能会影响它们。
WebView 的类型¶
有多种方法可以在 iOS 应用程序中包含 WebView
UIWebView
WKWebView
SFSafariViewController
UIWebView¶
UIWebView
从 iOS 12 开始已弃用,并且 不应使用。请确保使用 WKWebView
或 SFSafariViewController
嵌入 Web 内容。除此之外,无法为 UIWebView
禁用 JavaScript,这是不使用它的另一个原因。
WKWebView¶
WKWebView
是 iOS 8 中引入的,是扩展应用程序功能、控制显示内容(即,防止用户导航到任意 URL)和自定义的合适选择。
WKWebView
具有优于 UIWebView
的多个安全优势
- JavaScript 默认启用,但由于
WKWebView
的javaScriptEnabled
属性,它可以完全禁用,从而防止所有脚本注入缺陷。 JavaScriptCanOpenWindowsAutomatically
可用于防止 JavaScript 打开新窗口,例如弹出窗口。hasOnlySecureContent
属性可用于验证 WebView 加载的资源是否通过加密连接检索。WKWebView
实现进程外渲染,因此内存损坏错误不会影响主应用程序进程。
使用 WKWebView
和 UIWebView
时可以启用 JavaScript Bridge。有关更多信息,请参见下面的 “通过 WebView 公开的本地功能”部分。
SFSafariViewController¶
SFSafariViewController
从 iOS 9 开始可用,应用于提供通用的 Web 查看体验。这些 WebView 可以很容易地被发现,因为它们具有以下元素的特征布局
- 带有安全指示器的只读地址字段。
- 操作(“分享”)按钮。
- 完成按钮、后退和前进导航按钮以及用于直接在 Safari 中打开页面的“Safari”按钮。
有几件事需要考虑
- 无法在
SFSafariViewController
中禁用 JavaScript,这是当目标是扩展应用程序的用户界面时建议使用WKWebView
的原因之一。 SFSafariViewController
还与 Safari 共享 Cookie 和其他网站数据。- 用户与
SFSafariViewController
的活动和交互对应用程序不可见,该应用程序无法访问自动填充数据、浏览历史记录或网站数据。 - 根据 App Store 审核指南,
SFSafariViewController
不得被其他视图或图层隐藏或遮盖。
这对于应用程序分析应该足够了,因此 SFSafariViewController
超出了静态和动态分析部分的范围。
Safari Web Inspector¶
在 iOS 上启用 Safari Web Inspector 允许您从 macOS 设备远程 检查 WebView 的内容。这在应用程序使用 JavaScript Bridge 公开本机 API(例如,混合应用程序)时特别有用。
Safari Web Inspector 要求应用程序具有 get-task-allowed
授权。Safari 应用程序默认具有此授权,因此您可以查看加载到其中的任何页面的内容。但是,从 App Store 安装的应用程序将没有此授权,并且无法附加。在越狱设备上,您可以通过安装 GlobalWebInspect 将此授权添加到任何应用程序。然后,您可以将主机上的 Safari 附加到检查 WebView 的内容(请参见 附加到 WKWebView)。
通过 WebView 公开的本地功能¶
在 iOS 7 中,Apple 引入了允许 WebView 中 JavaScript 运行时与本机 Swift 或 Objective-C 对象之间进行通信的 API。如果这些 API 使用不当,重要的功能可能会暴露给设法将恶意脚本注入 WebView 的攻击者(例如,通过成功的跨站点脚本攻击)。
UIWebView
和 WKWebView
都提供了 WebView 与本机应用程序之间的通信方式。暴露给 WebView JavaScript 引擎的任何重要数据或本机功能也可以被 WebView 中运行的恶意 JavaScript 访问。
UIWebView
本机代码和 JavaScript 可以通信有两种基本方式
- JSContext:当 Objective-C 或 Swift 代码块分配给
JSContext
中的标识符时,JavaScriptCore 会自动将代码块包装在 JavaScript 函数中。 - JSExport 协议:在
JSExport
继承的协议中声明的属性、实例方法和类方法会映射到所有 JavaScript 代码可用的 JavaScript 对象。JavaScript 环境中对象的修改会反映在本机环境中。
请注意,只有在 JSExport
协议中定义的类成员才能被 JavaScript 代码访问。
WKWebView
WKWebView
中的 JavaScript 代码仍然可以将消息发回本机应用程序,但与 UIWebView
相比,无法直接引用 WKWebView
的 JSContext
。相反,使用消息传递系统和使用 postMessage
函数来实现通信,该函数会自动将 JavaScript 对象序列化为本机 Objective-C 或 Swift 对象。使用方法 add(_ scriptMessageHandler:name:)
配置消息处理程序。
应用权限¶
与每个应用程序都在自己的用户 ID 上运行的 Android 不同,iOS 使所有第三方应用程序都在非特权 mobile
用户下运行。每个应用程序都有一个唯一的根目录并且是沙盒化的,因此它们无法访问受保护的系统资源或系统或其他应用程序存储的文件。这些限制通过沙盒策略(又名配置文件)实现,这些策略由 Trusted BSD (MAC) 强制访问控制框架通过内核扩展强制执行。iOS 将一个通用的沙盒配置文件应用于所有第三方应用程序,称为容器。可以访问受保护的资源或数据(某些也称为 应用程序功能),但它受到称为授权的特殊权限的严格控制。
某些权限可以由应用程序的开发人员配置(例如数据保护或 Keychain 共享),并且将在安装后直接生效。但是,对于其他权限,第一次应用程序尝试访问受保护的资源时,会明确地询问用户,例如
- 蓝牙外围设备
- 日历数据
- 相机
- 联系人
- 健康共享
- 健康更新
- HomeKit
- 位置
- 麦克风
- 运动
- 音乐和媒体库
- 照片
- 提醒事项
- Siri
- 语音识别
- 电视提供商
即使 Apple 敦促保护用户的隐私并且 非常清楚如何请求权限,仍然可能发生应用程序出于非显而易见的原因请求太多权限的情况。
验证某些权限(例如相机、照片、日历数据、运动、联系人或语音识别)的使用应该非常简单,因为它应该很明显应用程序是否需要它们来完成其任务。让我们考虑以下有关照片权限的示例,如果授予该权限,应用程序可以访问“相机胶卷”中的所有用户照片(这是 iOS 用于存储照片的默认系统范围位置)
- 典型的二维码扫描应用程序显然需要相机才能运行,但也可能请求照片权限。如果明确需要存储,并且取决于正在拍摄的照片的敏感性,这些应用程序可能最好选择使用应用程序沙盒存储,以避免其他应用程序(具有照片权限)访问它们。有关敏感数据存储的更多信息,请参见 “iOS 上的数据存储”一章。
- 某些应用程序需要上传照片(例如用于个人资料图片)。最新版本的 iOS 引入了新的 API,例如
UIImagePickerController
(iOS 11+) 及其现代 替代PHPickerViewController
(iOS 14+)。这些 API 在与您的应用程序不同的进程上运行,通过使用它们,应用程序可以专门获得对用户选择的图像的只读访问权限,而不是对整个“相机胶卷”的访问权限。这被认为是避免请求不必要权限的最佳实践。
验证其他权限(如蓝牙或位置)需要更深入的源代码检查。应用程序可能需要它们才能正常运行,但这些任务处理的数据可能没有得到适当的保护。
当收集或简单地处理(例如缓存)敏感数据时,应用程序应提供适当的机制,让用户可以控制它,例如,能够撤销访问权限或删除它。但是,敏感数据不仅可能被存储或缓存,还可能通过网络发送。在这两种情况下,必须确保应用程序正确遵循适当的最佳实践,在这种情况下,包括实施适当的数据保护和传输安全性。有关如何保护此类数据的更多信息,请参见“网络 API”一章。
正如您所看到的,使用应用程序功能和权限主要涉及处理个人数据,因此是保护用户隐私的问题。有关更多详细信息,请参见 Apple Developer Documentation 中的文章 “保护用户的隐私” 和 “访问受保护的资源”。
设备能力¶
App Store 使用设备能力来确保仅列出兼容设备,因此允许下载该应用程序。它们在应用程序的 Info.plist
文件中的 UIRequiredDeviceCapabilities
键下指定。
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
通常你会找到
arm64
能力,这意味着该应用程序是为 arm64 指令集编译的。
例如,应用程序可能完全依赖 NFC 才能工作(例如 “NFC Tag Reader” 应用程序)。根据 归档的 iOS 设备兼容性参考,NFC 仅从 iPhone 7(和 iOS 11)开始可用。开发人员可能希望通过设置 nfc
设备能力来排除所有不兼容的设备。
关于测试,你可以将 UIRequiredDeviceCapabilities
视为应用程序正在使用某些特定资源的仅仅指示。与与应用程序能力相关的授权不同,设备能力不授予对受保护资源的任何权利或访问权限。可能需要其他配置步骤,这些步骤对于每种能力都非常具体。
例如,如果 BLE 是应用程序的核心功能,Apple 的 Core Bluetooth 编程指南 解释了要考虑的不同事项
- 可以设置
bluetooth-le
设备功能,以限制不具备 BLE 功能的设备下载其应用。 - 如果需要 BLE 后台处理,应添加诸如
bluetooth-peripheral
或bluetooth-central
(均为UIBackgroundModes
) 的应用功能。
但是,这还不足以使应用程序获得蓝牙外围设备的访问权限,Info.plist
文件中必须包含 NSBluetoothPeripheralUsageDescription
键,这意味着用户必须主动授予权限。有关更多信息,请参见下面的“Info.plist 文件中的目的字符串”。
权利¶
权利是签署到应用中的键值对,允许超出运行时因素(如 UNIX 用户 ID)的身份验证。 由于权利是经过数字签名的,因此无法更改。 系统应用和守护程序广泛使用权利来执行特定的特权操作,否则需要以 root 身份运行该进程。 这大大降低了受损系统应用或守护程序提升权限的潜在可能性。
可以使用 Xcode 目标编辑器的“摘要”选项卡设置许多权利。 其他权利需要编辑目标的权利属性列表文件,或从用于运行应用的 iOS 配置文件继承。
权利来源:
- 嵌入在用于代码签名应用的配置文件中的权利,配置文件由以下内容组成
- 在 Xcode 项目的目标“功能”选项卡上定义的功能,和/或
- 在证书、ID 和配置文件网站的“标识符”部分配置的应用程序的 App ID 上启用的服务。
- 由配置文件生成服务注入的其他权利。
- 来自代码签名权利文件的权利。
权利目标:
- 应用的签名。
- 应用的嵌入式配置文件。
Apple Developer Documentation 还解释说
- 在代码签名期间,与应用程序启用的功能/服务相对应的权利将从 Xcode 选择用于签署应用程序的配置文件传输到应用程序的签名。
- 配置文件在构建期间嵌入到应用程序包中 (
embedded.mobileprovision
)。 - Xcode 的“构建设置”选项卡中“代码签名权利”部分中的权利将传输到应用程序的签名。
例如,如果要设置“默认数据保护”功能,则需要转到 Xcode 中的 功能 选项卡并启用 数据保护。 Xcode 会将其直接写入 <appname>.entitlements
文件,作为默认值为 NSFileProtectionComplete
的 com.apple.developer.default-data-protection
权利。 在 IPA 中,我们可能会在 embedded.mobileprovision
中找到它,如
<key>Entitlements</key>
<dict>
...
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
</dict>
对于其他功能(如 HealthKit),必须征求用户的许可,因此仅添加权利是不够的,必须将特殊键和字符串添加到应用程序的 Info.plist
文件中。
Info.plist 文件中的目的字符串¶
目的字符串 或使用说明字符串是在请求访问受保护的数据或资源时,在系统的权限请求警报中提供给用户的自定义文本。
如果在 iOS 10 或更高版本上链接,则开发人员需要在其应用的 Info.plist
文件中包含目的字符串。 否则,如果应用程序尝试访问受保护的数据或资源,但没有提供相应的目的字符串,则 访问将会失败,应用程序甚至可能会崩溃。
有关不同的目的字符串 Info.plist 键的概述,请参见 iOS 的 Apple App Programming Guide 中的表 1-2。 单击提供的链接以查看 CocoaKeys reference 中每个键的完整说明。
代码签名权利文件¶
某些功能需要一个 代码签名权利文件 (<appname>.entitlements
)。 它由 Xcode 自动生成,但也可以由开发人员手动编辑和/或扩展。
以下是 开源应用程序 Telegram 的权利文件示例,其中包括 App Groups 权利 (application-groups
)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>com.apple.security.application-groups</key>
<array>
<string>group.ph.telegra.Telegraph</string>
</array>
</dict>
...
</plist>
上面概述的权利不需要用户的任何其他权限。 但是,始终最好检查所有权利,因为应用程序可能会在权限方面过度询问用户,从而泄露信息。
如 Apple Developer Documentation 中所述,需要 App Groups 权利才能通过 IPC 或共享文件容器在不同应用程序之间共享信息,这意味着可以在设备上直接在应用程序之间共享数据。 如果应用程序扩展需要 与其包含的应用程序共享信息,则也需要此权利。
根据要共享的数据,使用另一种方法(例如通过后端)共享可能更合适,在这种情况下,可以潜在地验证此数据,从而避免被用户自己篡改。
进程间通信 (IPC)¶
在移动应用程序的实现过程中,开发人员可能会应用传统的 IPC 技术(例如使用共享文件或网络套接字)。 应该使用移动应用程序平台提供的 IPC 系统功能,因为它比传统技术成熟得多。 使用没有安全意识的 IPC 机制可能会导致应用程序泄漏或暴露敏感数据。
与 Android 丰富的进程间通信 (IPC) 功能相比,iOS 提供的应用程序之间通信的选项相当有限。 实际上,应用程序无法直接通信。 在本节中,我们将介绍 iOS 提供的不同类型的间接通信以及如何测试它们。 这是一个概述
- 自定义 URL Schemes
- 通用链接
- UIActivity 分享
- 应用程序扩展
- UIPasteboard
自定义 URL 方案¶
自定义 URL 方案 允许应用程序通过自定义协议进行通信。 应用程序必须声明对方案的支持并处理使用这些方案的传入 URL。
Apple 在 Apple Developer Documentation 中警告了不当使用自定义 URL 方案的行为
URL 方案为您的应用程序提供了一个潜在的攻击媒介,因此请确保验证所有 URL 参数并丢弃任何格式错误的 URL。 此外,将可用操作限制为那些不会危及用户数据的操作。 例如,不允许其他应用程序直接删除内容或访问有关用户的敏感信息。 在测试您的 URL 处理代码时,请确保您的测试用例包括格式不正确的 URL。
如果目的是实现深度链接,他们还建议使用通用链接代替
虽然自定义 URL 方案是一种可以接受的深度链接形式,但强烈建议将通用链接作为最佳实践。
支持自定义 URL 方案是通过
- 定义应用程序 URL 的格式,
- 注册方案,以便系统将适当的 URL 指向应用程序,
- 处理应用程序接收到的 URL。
当应用程序在未正确验证 URL 及其参数的情况下处理对其 URL 方案的调用时,以及当用户在触发重要操作之前未收到确认提示时,会出现安全问题。
一个例子是以下 Skype Mobile 应用程序中的错误,该错误于 2010 年发现:Skype 应用程序注册了 skype://
协议处理程序,该处理程序允许其他应用程序触发对其他 Skype 用户和电话号码的呼叫。 不幸的是,Skype 在拨打电话之前没有征求用户的许可,因此任何应用程序都可以在用户不知情的情况下拨打任意号码。 攻击者通过放置一个不可见的 <iframe src="skype://xxx?call"></iframe>
(其中 xxx
被替换为高级号码)来利用此漏洞,因此任何无意中访问恶意网站的 Skype 用户都会拨打高级号码。
作为开发人员,您应该在调用任何 URL 之前仔细验证它。 您可以仅允许某些可以通过注册的协议处理程序打开的应用程序。 提示用户确认 URL 调用的操作是另一种有用的控制手段。
所有 URL 都会传递到应用程序委托,无论是在启动时还是在应用程序运行或在后台运行时。 为了处理传入的 URL,委托应实现方法来
- 检索有关 URL 的信息并确定是否要打开它,
- 打开 URL 指定的资源。
可以在 存档的 App Programming Guide for iOS 和 Apple Secure Coding Guide 中找到更多信息。
此外,应用程序可能还希望向其他应用程序发送 URL 请求(又名查询)。 这是通过
- 注册应用程序要查询的应用程序查询方案,
- (可选)查询其他应用程序以了解它们是否可以打开特定 URL,
- 发送 URL 请求。
通用链接¶
通用链接是 iOS 等效于 Android 应用链接(又名数字资产链接),用于深度链接。 当点击通用链接(到应用程序的网站)时,用户将无缝重定向到相应的已安装应用程序,而无需通过 Safari。 如果未安装该应用程序,则该链接将在 Safari 中打开。
通用链接是标准 Web 链接 (HTTP/HTTPS),不应与最初也用于深度链接的自定义 URL 方案混淆。
例如,Telegram 应用程序同时支持自定义 URL 方案和通用链接
tg://resolve?domain=fridadotre
是自定义 URL 方案,并使用tg://
方案。https://telegram.me/fridadotre
是通用链接,并使用https://
方案。
两者都会导致相同的操作,用户将被重定向到 Telegram 中指定的聊天(在本例中为“fridadotre”)。 但是,通用链接提供了使用自定义 URL 方案时不可用的几个关键优势,并且是根据 Apple Developer Documentation 推荐的深度链接实现方式。 具体来说,通用链接是
- 唯一的:与自定义 URL 方案不同,通用链接不能被其他应用程序声明,因为它们使用标准 HTTP 或 HTTPS 链接到应用程序的网站。 它们被引入作为一种防止 URL 方案劫持攻击的方式(原始应用程序之后安装的应用程序可以声明相同的方案,并且系统可能会将所有新请求定向到最后安装的应用程序)。
- 安全的:当用户安装应用程序时,iOS 会下载并检查一个已上传到 Web 服务器的文件(Apple App Site Association 或 AASA),以确保网站允许应用程序代表其打开 URL。 只有 URL 的合法所有者才能上传此文件,因此他们的网站与应用程序的关联是安全的。
- 灵活的:即使未安装应用程序,通用链接也能正常工作。 点击网站的链接将在 Safari 中打开内容,正如用户所期望的那样。
- 简单的:一个 URL 适用于网站和应用程序。
- 私密的:其他应用程序可以与该应用程序通信,而无需知道它是否已安装。
您可以在 Carlos Holguera 的文章 “Learning about Universal Links and Fuzzing URL Schemes on iOS with Frida” 中了解更多关于通用链接的信息。
UIActivity 共享¶
从 iOS 6 开始,第三方应用程序可以通过特定机制 例如 AirDrop 共享数据(项目)。 从用户的角度来看,此功能是众所周知的系统范围的“共享活动表”,该表在单击“共享”按钮后出现。
可用的内置共享机制(又名活动类型)包括
- airDrop
- assignToContact
- copyToPasteboard
- message
- postToFacebook
- postToTwitter
可以在 UIActivity.ActivityType 中找到完整列表。 如果认为不适合该应用程序,开发人员可以选择排除其中一些共享机制。
应用程序扩展¶
与 iOS 8 一起,Apple 推出了应用程序扩展。 根据 Apple App Extension Programming Guide,应用程序扩展允许应用程序在用户与其他应用程序或系统交互时向其提供自定义功能和内容。 为了做到这一点,它们实现了特定的、范围明确的任务,例如,定义在用户单击“共享”按钮并选择某些应用程序或操作后发生的事情,为“今天”小部件提供内容或启用自定义键盘。
根据任务,应用程序扩展将具有特定的类型(并且只有一个),即所谓的扩展点。 一些值得注意的包括
- 自定义键盘:用自定义键盘替换 iOS 系统键盘,以便在所有应用程序中使用。
- 共享:发布到共享网站或与他人共享内容。
- 今天:也称为小部件,它们在通知中心的“今天”视图中提供内容或执行快速任务。
应用程序扩展如何与其他应用程序交互¶
这里有三个重要的元素
- 应用程序扩展:是捆绑在包含应用程序中的一个。 主机应用程序与之交互。
- 主机应用程序:是触发另一个应用程序的应用程序扩展的(第三方)应用程序。
- 包含应用程序:是包含捆绑在其中的应用程序扩展的应用程序。
例如,用户在主机应用程序中选择文本,单击“共享”按钮并从列表中选择一个“应用程序”或操作。 这会触发包含应用程序的应用程序扩展。 应用程序扩展在主机应用程序的上下文中显示其视图,并使用主机应用程序提供的项目(在本例中为选定的文本)来执行特定任务(例如,在社交网络上发布它)。 请参见 Apple App Extension Programming Guide 中的这张图片,它很好地总结了这一点
安全注意事项¶
从安全的角度来看,重要的是要注意
- 应用程序扩展永远不会直接与其包含的应用程序通信(通常,当包含的应用程序扩展运行时,它甚至没有运行)。
- 应用程序扩展和主机应用程序通过进程间通信进行通信。
- 应用程序扩展的包含应用程序和主机应用程序根本不进行通信。
- “今天”小部件(而不是其他应用程序扩展类型)可以通过调用
NSExtensionContext
类的openURL:completionHandler:
方法来要求系统打开其包含的应用程序。 - 任何应用程序扩展及其包含的应用程序都可以访问私下定义的共享容器中的共享数据。
此外
- 应用程序扩展无法访问某些 API,例如 HealthKit。
- 它们无法使用 AirDrop 接收数据,但可以发送数据。
- 不允许长时间运行的后台任务,但可以启动上传或下载。
- 应用程序扩展无法访问 iOS 设备上的摄像头或麦克风(iMessage 应用程序扩展除外)。
Pasteboard¶
使用 UIPasteboard
API,应用程序可以访问 iOS 粘贴板,从而允许它们在应用程序内部或跨应用程序共享数据。 但是,通用粘贴板的系统范围性质引发了隐私和安全问题,尤其是在未经用户交互的情况下以编程方式复制敏感数据时。
有两种类型的粘贴板
- 通用粘贴板 (
UIPasteboard.general
):在所有前台应用程序之间共享,并且通过 通用剪贴板,可能在 Apple 设备之间共享。 默认情况下,它在设备重启和应用程序重新安装之间持续存在,除非被清除。 从 iOS 16 开始,通用粘贴板需要用户交互才能访问。 - 自定义或命名粘贴板 (
UIPasteboard(name:create:)
和UIPasteboard.withUniqueName()
):这些是 私有粘贴板,它们是应用程序或团队特定的,即,仅限于创建它们的应用程序或来自同一团队 ID 的其他应用程序。 默认情况下,它们是非持久性的,因为从 iOS 10 开始(在应用程序终止和系统重启时删除)。 Apple 不鼓励使用持久性自定义粘贴板,并建议 使用 App Groups 在同一开发者的应用程序之间共享数据。
iOS 粘贴板 API 经历了多次更改,这些更改可能会影响用户的隐私和安全
- 自 iOS 9 以来,对粘贴板的访问已限制为在前台运行的应用程序,这大大降低了被动剪贴板嗅探的风险。 但是,如果敏感数据保留在粘贴板上,并且恶意应用程序稍后被带到前台(或者每当用户位于屏幕上的小部件时,它都会保持在前台),则该应用程序可以在未经用户同意或知情的情况下访问该数据。 请参阅 示例攻击。
- 自 iOS 10 以来,默认启用通用剪贴板,并且当用户登录 iCloud 时,使用同一 iCloud 帐户自动将通用粘贴板内容同步到用户附近的 Apple 设备。 开发人员可以选择通过使用
UIPasteboard.localOnly
将通用粘贴板的内容限制为本地设备来禁用此功能。 此外,他们可以使用UIPasteboard.expirationDate
设置粘贴板项目的过期时间。 - 自 iOS 14 以来,当应用程序读取由不同的应用程序写入的通用粘贴板内容而没有用户意图时,系统会通知用户。 系统根据用户交互(例如,点击系统提供的按钮或从上下文菜单中选择 粘贴)来确定用户意图。
- 自 iOS 16 以来,每当应用程序访问粘贴板内容时,系统都会提示用户显示粘贴确认对话框。 因此,对通用粘贴板的任何访问都必须由用户交互显式触发。 应用程序还可以使用
UIPasteControl
,通过在检测到兼容数据时显示一个特殊的“粘贴”按钮来处理粘贴操作。 这不一定更好或更安全; 这是对用户体验的改进。 它避免了每次都提示用户,但用户仍然需要点击,因此访问仅响应用户交互而发生。