保护你的应用:威胁建模与反模式
Secure your app: threat modeling and anti-patterns
2020年6月24日
一句话判断
大多数应用的安全漏洞不是因为加密算法不够强,而是因为开发者犯了常识性错误——这场 Session 用 STRIDE 威胁模型和真实反模式案例帮你系统性地堵住这些漏洞。
这场 Session 讲了什么
这场 Session 从软件安全工程的角度讲解了如何系统性地保护你的应用。它不是教你怎么用某个 API,而是教你一种思维方式——威胁建模(Threat Modeling)。Session 使用了微软的 STRIDE 框架来分类威胁:Spoofing(欺骗)、Tampering(篡改)、Repudiation(否认)、Information Disclosure(信息泄露)、Denial of Service(拒绝服务)、Elevation of Privilege(权限提升)。
对每种威胁类型,Session 都给出了移动应用中的具体场景和防御策略。比如 Spoofing 可能是攻击者伪造 API 请求中的用户身份;Tampering 可能是中间人修改传输中的数据;Information Disclosure 可能是日志中意外打印了用户密码。每个场景都配有代码级的反模式示例——那些”看起来没问题但实际上很危险”的写法。
Session 特别强调了”纵深防御”(Defense in Depth)原则:不要只依赖一层安全措施。即使你用了 HTTPS,也应该对敏感数据做本地加密;即使你验证了用户身份,也应该在后端再次校验权限。每一层防御都是一道独立的屏障,任何单一环节的失守都不应该导致整个系统的崩溃。
值得深挖的点
常见安全反模式
Session 列举了几个高频反模式:将 API Key 硬编码在源码中(攻击者反编译后一搜就找到)、用 UserDefaults 存储密码和 token(未加密的 plist 文件)、在 URL 中传递敏感参数(会被日志和代理记录)、不验证 TLS 证书(allowsAnyHTTPSCertificate 是最危险的代码之一)、以及没有对用户输入做服务端校验(客户端校验可以被绕过)。
Keychain vs UserDefaults 的安全边界
UserDefaults 以未加密的 plist 文件形式存储在磁盘上,任何有设备物理访问权限的人都可以读取。Keychain 使用硬件级别的加密,即使设备被越狱,没有用户的 passcode 也无法读取加密的 Keychain 条目。Session 的规则很简单:任何你不希望出现在明文日志中的数据,都应该存在 Keychain 中。
代码片段
import Security
import Foundation
// 正确:使用 Keychain 存储敏感数据
class SecureKeychain {
static func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
// 关键:ThisDeviceOnly 确保数据不会通过 iCloud Keychain 同步到其他设备
]
// 先删除已有的条目
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else { return nil }
return result as? Data
}
// 反模式:绝对不要这样做
// UserDefaults.standard.set("my_api_key_12345", forKey: "api_key")
// UserDefaults 文件是明文 plist,任何有物理访问权限的人都能读取
}
// 使用示例
let token = "user_auth_token_xxx".data(using: .utf8)!
SecureKeychain.save(key: "auth_token", data: token)
if let savedToken = SecureKeychain.load(key: "auth_token") {
print("Token 已安全存储")
}
// 安全的网络请求配置
import Foundation
class SecureNetworkClient {
let session: URLSession
init() {
let config = URLSessionConfiguration.default
// 安全配置
config.urlCache = nil // 敏感请求不缓存
config.requestCachePolicy = .reloadIgnoringLocalCacheData
// 使用系统的证书验证(不要关闭它!)
// 反模式:sessionDelegate 中信任所有证书
// func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
// completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// completionHandler(.useCredential, challenge.protectionSpace.serverTrust.map { URLCredential(trust: $0) })
// // 这样写等于没有 TLS 保护
// }
session = URLSession(configuration: config)
}
func makeAuthenticatedRequest(url: URL, token: String) {
var request = URLRequest(url: url)
request.httpMethod = "GET"
// 正确:token 通过 HTTP Header 传递
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// 反模式:token 放在 URL 查询参数中
// let url = URL(string: "https://api.example.com/data?token=\(token)")!
// URL 会被代理、日志、浏览器历史记录完整保存
session.dataTask(with: request) { data, response, error in
// 处理响应
}.resume()
}
}
// 数据保护与文件加密
import Foundation
class SecureFileStorage {
// 使用文件保护 API
func saveProtectedFile(data: Data, to url: URL) throws {
// 写入文件
try data.write(to: url, options: .completeFileProtection)
// .completeFileProtection: 设备锁定时文件不可读
// 更严格保护
// .completeFileProtectionUnlessOpen: 锁定时不可读,但已打开的文件可以继续访问
// .completeFileProtectionUntilFirstUserAuthentication: 首次解锁前不可读
}
// 检查 App Transport Security 配置
func verifySecurityConfiguration() {
// 确保 Info.plist 中没有 NSAllowsArbitraryLoads = YES
// 这会允许所有 HTTP 连接,严重降低安全性
// 反模式:
// <key>NSAppTransportSecurity</key>
// <dict>
// <key>NSAllowsArbitraryLoads</key>
// <true/>
// </dict>
// 正确做法:只对需要的主机豁免
// <key>NSExceptionDomains</key>
// <dict>
// <key>legacy-server.example.com</key>
// <dict>
// <key>NSExceptionAllowsInsecureHTTPLoads</key>
// <true/>
// </dict>
// </dict>
}
}
最佳实践
- 所有敏感数据(token、密码、API Key)必须存储在 Keychain 中,设置
kSecAttrAccessibleWhenUnlockedThisDeviceOnly - 永远不要在代码中硬编码密钥和 token——使用环境变量或运行时从安全存储中获取
- 保持 App Transport Security 开启,只在必要时对特定域名做最小化豁免
- 在服务端验证所有用户输入和权限——客户端的验证是为了 UX,不是为了安全
- 定期使用 Xcode 的 Security Audit 工具和 static analyzer 检查代码中的安全漏洞
还有什么值得关注
- Session 提到了 Apple 的 Security Framework 中新增了 CryptoKit 的高级功能,建议使用 CryptoKit 而非 CommonCrypto
- TLS 1.3 在 iOS 14 中默认启用,你的服务器也应该尽快支持
- 对于企业级应用,MDM 可以远程擦除设备上的 Keychain 数据,这是应对设备丢失的重要安全措施