
保护你的应用程序:避免api密钥被盗的5个关键策略
- Rifx.Online
- Security , Best Practices , AI Applications
- 23 Feb, 2025
人工智能的淘金热正在如火如荼地进行中。
在淘金热中,坏人会偷走铲子。
一个致命的错误是常见的:将 ChatGPT API 密钥直接捆绑到您闪亮的新 AI 应用中。这使得 盗取您的密钥、消耗您的额度 和 累积高额账单 变得非常简单。
今天,我将解释一个基本的 安全法则:
不要在客户端存储 API 密钥。绝对不要。
我将演示两种技术,允许坏人访问您的 API 密钥,并向您展示如何避免同样的命运。
反编译您的应用
如果您在设备上存储 API 密钥,您可能会采用各种复杂程度来保护它们。我将详细介绍最流行的方法,并演示它们是多么容易被绕过。
原始字符串在代码中
这通常是工程师在变得安全意识强之前采取的第一种方法。当从 API 请求数据时,您会写类似这样的代码:
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = "12345678-90ab-cdef-1234-567890abcdef"
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
这段代码被编译并以 .app
包的形式交付给用户,包含所有可执行代码。
我之前写过关于如何轻松获取应用商店中任何应用的 .ipa
文件的文章,该文件包含这个 .app
包。这是恶意行为者窃取您密钥的第一步。
如果您想轻松阅读,可以查看 通过反编译他们的应用包在面试中给人留下深刻印象!
通过这种方法,查看 .app
包内部是微不足道的。它只是一个文件夹,看起来像这样。
真正的重点在于 Bev,这是 我值得信赖的酒精副项目 的 UNIX 可执行文件。它包含了我应用的所有 编译和链接代码。
使用内置的 UNIX 命令 strings
,我们可以检查这个编译的可执行文件中每个原始字符串。
自然,输出非常冗长:
除了原始字符串,您还会看到许多编译函数的符号名称。
但在这个列表的底部,我们看到了我们的奖品:
12345678-90ab-cdef-1234-567890abcdef
我们的 API 密钥的原始字符串,暴露在外——任何精通正则表达式的人都可以轻松搜索到。
Info.plist
你可能会想,好吧,我不会把原始字符串放入我的应用程序中,以便它们可以被轻易反编译。
所以我会把它们放在 Info.plist
中,这是它们_应该_在的地方。
你的 API 调用位置变得有点复杂,以获取这个值。
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = getAPIKeyPlist()
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
func getAPIKeyPlist() -> String? {
Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String
}
这里有几个复杂的子层级。与其直接硬编码密钥,你可能有多个密钥集,用于开发和生产。你可以为每个环境创建 .xcconfig
文件,这些文件在构建时解析为 Info.plist
中的值。
如果你是一个安全狂热者,你会知道绝不能将秘密提交到源代码控制中。更进一步,也许你会在你的 CI/CD 管道中安全地存储你的 API 密钥,将它们注入到 .xcconfig
或 Info.plist
中,作为构建脚本的一部分。
但这一切都是徒劳的。
因为让我们再看看那个反编译的应用程序,任何拥有 MacBook、iPhone 和过多闲暇时间的人都可以获取:
糟糕。
无论它们是如何到达那里的,放入 Info.plist
中的值都是公开可用的。
混淆字符串
如果你不加批判地听信你在互联网上阅读的所有内容,你可能会认为通过混淆你的字符串来保护自己是很聪明的。2019年我第一次决定成为常驻安全专家时,我确实是这么想的。
你可以对你的混淆算法和加盐做得非常复杂,但实际上,你会让 ChatGPT 为你编写一对函数,以混淆和解码你的 API 密钥。
private func obfuscate(key: String) -> String? {
let salt = "00000000-0000-0000-0000-000000000000"
guard let keyData = key.data(using: .utf8),
let saltData = salt.data(using: .utf8) else {
return nil
}
let obfuscatedData = zip(keyData, saltData).map { $0 ^ $1 }
let obfuscatedBase64 = Data(obfuscatedData).base64EncodedString()
return obfuscatedBase64
}
private func decode(obfuscated: String) -> String? {
let salt = "00000000-0000-0000-0000-000000000000"
guard let obfuscatedData = Data(base64Encoded: obfuscated),
let saltData = salt.data(using: .utf8) else {
return nil
}
let apiKeyData = zip(obfuscatedData, saltData).map { $0 ^ $1 }
return String(bytes: apiKeyData, encoding: .utf8)
}
在运行时获取密钥就像解码原始混淆字符串一样简单。
func callChatGPTAPI(prompt: String) async throws -> Data {
let url = URL(string: "https://api.chatgpt.com")!
var request = URLRequest(url: url)
let apiKey = getAPIKeyObfuscated()
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: request).0
}
func getAPIKeyObfuscated() -> String? {
decode(obfuscated: "AQIDBAUGBwgACQBRUgBTVFVWAAECAwQABQYHCAkAUVJTVFVW")
}
虽然这比原始字符串稍微难以阅读,但对于一个足够有动机的黑客来说,窃取你的密钥仍然很简单。
如果你使用不安全的方法,例如 base-64 编码或异或,逆向工程是微不足道的。即使有一个不错的算法和盐,攻击者也可以使用像Frida这样的工具在运行时从设备内存中提取你的 API 密钥。
混淆提供了_模糊带来的安全性_,但这并不是一个有效的方法。无论你的混淆多么优秀,那个 API 密钥最终都必须被解码并在某个时刻发送到 API。
这引出了我们最后也是最简单的窃取 API 密钥的方法。
监控网络流量
这是您可以使用的最强大技术之一。
像 Proxyman(或者如果您是老玩家,可以使用 Charles 代理)这样的免费工具允许您检查应用程序或网站的所有进出流量。
在加载程序的几秒钟内,我们立即在网络请求中发现包含我们 API 密钥的授权头。
12345678-90ab-cdef-1234-567890abcdef
因为我很奇怪,这就是我在开始新工作之前喜欢做的 另一件事:检查一些网络流量,以找出他们是 REST 还是 GraphQL。
减轻代理攻击
可以使用 SSL 钉扎等技术来减轻这种攻击向量。SSL 钉扎检查后端服务器提供的 TLS 证书,以确保它们与存储在设备上的证书匹配。这可以防止中间人攻击,并阻止网络代理工具的代理根证书。
要在 iOS 上实现 SSL 钉扎,可以创建一个使用 URLSessionDelegate
的网络请求装饰器,该装饰器实现 urlSession(didReceive: URLAuthenticationChallenge)
。
但是,你猜对了,SSL 钉扎并不一定如人们所想的那样完美。反编译者可以使用工具来绕过钉扎,尤其是在使用越狱设备时。
此外,当你不知道自己在做什么时,SSL 钉扎可能会带来风险。
当我还是一名相对初级的工程师,急于证明自己时,我天真地使用 AWS 拥有的 EC2 根证书实现了 SSL 钉扎。自然,AWS 在同一周轮换了证书。长话短说,英国的 COVID-19 测试中心在我生命中最长的 59 分钟内变得无法使用。
我应该做什么?
我希望到现在为止,你已经内化了我们的主要教训:保护客户端 API 密钥在很大程度上是安全表演。如果你将 API 密钥打包到公开分发的应用程序中,它们将永远不会安全。
既然我们知道了不该做什么,我们能做什么呢?
虽然客户端本质上不安全,但保护你的后端免受恶意行为者的攻击要简单得多。保护第三方 API 密钥的“教科书”方法是将它们放在一个(轮换的)秘密管理器中,并通过你的后端代理网络请求。
通过这种方法,你可以使用自己的系统对网络请求进行身份验证,并最小化潜在的攻击向量。
如果你还没有完全构建自己的基础设施,Firebase Cloud Functions 是实现这个中间件层的简单方法。
顺便提一下,你可以通过像 iOS 上的 AppAttest、Android 上的 Play Integrity 和实施 RASP (运行时应用程序自我保护) 库 来为你的设备添加更多保护层。这些工具可以阻止你的应用在越狱设备上运行,并防止逆向工程工具的使用。
对我的建议的小补充:如果你正在使用 Firebase,我发现 AppAttest 存在很大的问题。也许他们已经修复了,但我从不放过抱怨的机会。
结论
不要在客户端存储 API 密钥。
就像,绝对 不要。
任何告诉你 API 密钥在客户端是安全的人,要么是愚蠢的,要么是恶意的。
任何打包到你的应用程序中的东西都可以被检查。原始字符串和 Info.plist
文件中的 API 密钥很容易被找到,而更复杂的混淆方法仍然容易受到有动机攻击者的攻击。
网络代理是一个强大的工具,既可以用于善,也可以用于恶。保护机制如 SSL 钉扎如果你不知道自己在做什么,可能会带来更多麻烦。客户端保护机制,如应用完整性检查或反编译检测库,有助于为你的应用或网站增加保护层。
如果你想合理地保护你的 API 密钥,你需要将所有内容存储在后端,或者使用 Firebase Cloud Functions 等产品。你可以安心睡觉,知道你的 API 密钥是安全的。可能是。
Jacob’s Tech Tavern 对于你在遵循我的建议后产生的 $5,000 ChatGPT 或 Firebase 账单不承担任何责任。