Python实战社群
Java实战社群
长按识别下方二维码,按需求添加
扫码关注添加客服
进Python社群▲
扫码关注添加客服
进Java社群▲
作者:keenleopard, 学生时代搞量子计算,现在在字节跳动平台架构部搞 iOS 架构
Sessions: https://developer.apple.com/videos/play/wwdc2020/10189/
安全风险也是一种 bug,是一个比较大的 topic,通常比较难发现,但会产生比较严重的影响。本次技术分享意图通过一些基本的概念和例子来初步探讨怎么样在开发过程中规避风险。
「威胁建模(TM)」是风险管理中的一个概念,指的是寻找系统潜在的威胁,在这种威胁的基础上添加对抗的策略。通常可以从三个方向入手:
攻击者导向 Attacker-centric:假设你是一个攻击者,比如用户的朋友或者网络请求的中间人,去攻击用户数据
系统导向(或软件导向)System/Software-centric:从软件的设计模式出发,寻找各个模块之间不合理的地方,这方面做的比较好的例子是微软的 SDLC
资产导向 Asset-centric:从用户本地数据/服务端数据出发,寻求突破点。
对于客户端 RD 来说,我们可以把这些理念应用于:
新开发产品/新需求的时候
解决已成型产品的问题
比如说,我们为奶茶爱好者开发了一个新的 App,名字叫 LooseLeaf,支持以下功能
和朋友分享喝茶
上传照片、视频,甚至直播茶道、茶文化
建立圈子文化,有兴趣小组和群聊功能
点评点心、饼干
提供推荐和对比
(实际上,这些是不是很多产品的形态?)
那么,在搭建这个产品的时候,我们怎么把 TM 包含进来呢?
比如说,对 App 来说,很关键的一个概念是 Assets(资产),这里不仅仅指的是 Xcode 里面的那个 Asset Catelog 里面的资源,更重要的是用户的信任。所有需要用户授权 App 才能获取的内容都是 Assets,比如相机/麦克风的使用、地理位置信息、联系人信息、好友列表、文件等,App 需要履行保护这些 Assets 的责任。初此之外,用户的观点、评价等信息也是非常重要的 Asset。
典型的攻击方包括:
违法行为如诈骗
竞争对手
服务提供商
主权国家
伴侣
家人和朋友
作为开发者,我们要问自己的问题是,针对不同种类的攻击方,我们怎么保护资产?这些攻击方有着不同程度的资产接触能力,比如通过获取硬件,或者通过共享的账号。
作为 RD 来说,我们可能不太关心谁是攻击者,而更关心攻击者的手段。为了更好的管理数据,一个比较好的手段是建立数据流(Data Flow Diagram)。
比如这个 Loose Leaf 的可能用到的数据包括:
用户上传到LooseLeaf网站或者icloud的数据,比如账号信息、PUGC内容
缓存下来的远端的数据(通常这部分会被用作本地持久化)
用户授权的相机、麦克风、地理位置等系统权限
Schema,从而可以通过网站或者其他App打开我们的App
在这个 Data Flow Diagram 的基础上,我们需要明确在系统中明确一个「安全墙」。这个是我们的一个出发点,它需要贯穿我们所有的需求开发当中,因为它告诉我们可以做什么、有什么数据我们不能相信,以及需要做什么。
怎么样确认「安全墙」呢?
我们可以问自己一个问题:攻击者可以在哪里、通过什么样的方式影响我们的App,他们能获取什么样的数据。比如上面的例子中,以下几个地方都应该有安全墙:
系统文件与 App 之间
schma 携带的数据
App 与远端或者 iCloud 之间
针对每个安全墙,都要从架构层面考虑合理的对抗方式,比如在网络通信层面使用 AppTransportSecurity。我们需要考虑怎么样更安全地在本地或者服务端存储数据。总的来说,我们应该选择可以方便解析的数据格式,plist 或者 json 都是比较好的格式,可以在灵活性与安全性之间得到平衡。我们应该采用那些可以做 schema 校验的格式,因为它们支持强类型校验和中心逻辑。
上面这个图中,我们并没有在 App 与缓存的数据之间建立安全墙。这里的原因是,我们应当在存储数据的时候就做了安全校验,因此所有本地存储的数据是可以被信任的。对于 iOS App 而言,这一点是得到保证的,因为它的本地文件不能被其他的 App 更改。但是对于 MacOS 的 App 而言,事情确不是这样的。因此作为开发者,如果要适配 MacOS,也需要考虑怎么样在 Mac 上保护本地文件。
我们在开发软件的时候经常要使用三方库,在这个过程中需要考虑三方库的安全隐私条款是否与我们 App 匹配,以及是否可以快速检测出不匹配的场景。
基本的思路理清之后,我们可以应用在代码层面。比如在 Loose Leaf 中,使用 MVC 的设计模式可以有三种不同功能的 class:
负责从外部(包含远端、本地、系统)中得到数据(黄色)
负责解析数据并转换成 Model(红色)
负责将数据展示成 UI(绿色)
我们需要理清楚每个类的功能是什么样的,哪里具有高风险。
存储在远端的数据是最高风险的地方之一,因为上传到 Loose Leaf 网站的数据可以被攻击者任意创造。
我们顺着 data flow,可以根据风险来建立不同级别的 risk profile:
不同级别的风险:红色标示风险最高,绿色标示风险最低。总结:在威胁建模(TM)中,开发者需要注意这几点:
明确你的系统的数据资产
从架构层面考虑风险规避,包括传输和存储,例如
网络通信
本地存储
三方库的依赖
确认哪些数据是高风险的、不可信任的、可以被攻击者控制的
根据data flow来追踪这些风险数据,从而确认出系统中的哪些高风险组件
明确什么样的数据是不可信的是提升我们 App
全性的第一步。怎么样知道哪些数据是不可信的呢?
答案:
所有我们不能掌控的、来自于外部的数据都应该被当作不可信数据
如果你不知道是否能掌控某个数据,那么就当作不能掌控。记住,总要做最坏假设!
举个例子,Loose Leaf 使用了自定义的 URL handler,那么传过来的 URL 就完全不是可以信任的。用户可以通过各种途径(比如朋友发来的微信)来获取到这样的 URL:looseleaf://invite?payload=aW5zZXJQ3ONGHwdlMV9kYm94。类似的,如果你的 App 支持一些自定义的格式,那么传来的文件也可能攻击你的 App。
最典型的一个场景是来源于网络,这种包括
HTTPS 或者 WebSocket
P2P通信(如视频通话)
蓝牙传输
在理清什么样的数据不可信任之后,我们列举出一些典型的错误代表,看看开发过程中经常遇到的错误。
(比如iOS广泛使用的SSZipArchive, github issue[1])
比如在 Loose Leaf 这个 App 中,我们开发了一个照片分享的功能:刘能可以把奶茶照片发给他的朋友赵四。在 App 中定义了这个方法,赵四在下载下来照片后这个照片会被临时复制到一个目录当中,防止被系统删除:
- (void)handleIncomingFile:(NSURL *)incomingResourceURL
withName:(NSString *)name
from:(NSString *)fromID {
NSURL *destinationFileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:name]];
NSError *err = nil;
[[NSFileManager defaultManager] copyItemAtURL:incomingResourceURL
toURL:destinationFileURL
error:&err];
}
上面这种写法有什么问题?三个入参中哪些是不可信的?
incomingResourceURL
是下载图片的时候赵四的手机本地生成的一个URL,不会被刘能的手机控制,因此是可信的;
fromID
是用户id,也是可信的;
name
则完全是刘能掌控的一个字符串,它可以是一个普通的名字如“村头的喜茶”,但也可以是任意的字符串,因此是不可信的。比如name可以是../../Library/Foo/
,在这种情况下,上面代码段的第四行就变成了
NSURL *destinationFileURL = [NSURL filewithPath:@"/var/mobile/Containers/Data/A123/tmp/../../Library/Foo/"]
。
因此刘能可以控制自己的照片被拷贝到赵四手机这个App的沙盒目录的任何地方,后果会非常严重,因为沙盒中的一些敏感的数据可能被改写。
怎么样规避这个问题呢?如果使用name.lastPathCompanent
来替换第四行:
[NSURL fileURLwithPath:NSTemporaryDirectory() stringByAppendingPathComponent:name.lastPathCompanent]
,在上面这个例子中还可以,但是lastPastCompanent
依然可能会出现类似于..
这种bad case,所以也是有问题的。
所以需要在上面这个函数一开始的时候做一些bad case的过滤:
NSString *safeFileName = name.lastPathComponent;
if (safeFileName.length == 0 ||
[safeFileName isEqualToString: @".."] ||
[safeFileName isEqualToString:@"."] ) {
return;
}
如果是swift项目的话,通过guard来实现更简洁一些:
guard
case let safePath = name.lastPathComponent,
safePath.count > 0,
safePath != "..",
safePath != "."
else { return }
因此,在filePath
中尽量不要不可控的或者远端传来的参数,而使用本地随机生成的参数。如果必须要使用的话,过滤出lastPathComponent
并做数据校验和数据清洗。此外,也需要使用fileURLWithPath:
从而防止 percentage encoding attack。
作为客户端 RD,现在就可以检查你是否在使用下面这些API的时候在路径中传入了不可信任的字符串:
- appendingPathComponent:
- pathWithComponents:
- fileURLWithPathComponents:
- URLByAppendingPathComponent
比如远端往一个设备上发了一个请求,然后这个请求在本地解析一下发个响应回去,一个简单的例子:
- (NSString *)generateResponseFromRequest:(NSString *)requestFormat withName:(NSString *)name {
NSString *formatStringKey = requestFormat ?: @"%@ sent a response";
NSString *localizedFormatString = [NSString localizedStringWithFormat:formatStringKey, name];
return localizedFormatString;
}
两个参数中,name
是本地的一个变量,是可信的;而requestFormat
是远端传入参数,因此是不可信的。假如传入的requestFormat = @"%@ leaked some memory %lx%lx%lx%lx%lx"
,那么第三行就变成了这个样子[NSString localizedStringWithFormat:@"%@ leaked some memory %lx%lx%lx%lx%lx", @"Jack"]
;
没有足够的变量用于解析Format,造成了溢出,实际输出的将是:
Jack leaked some memory cbd4309b6142d3d1418c1915116b5b32a28e7ecfa630245f
因此需要注意的是:
计量不要直接把远端的数据当作 format string
确保编译 flag -Wformat-security 是打开的(默认是开的),但是上面这个 case 它检测不出来,能检测出来的是这种 case:
static void foo(const char *input) {
printf(input); //warning: format string is not a string literal (potentially insecure)
}
在Swift中不要使用format string,要使用string interpolation
现在就可以检测一下在下面这些API当中是否传入了不可信任的string:
+[NSString stringWithFormat:]
-[NSMutableString appendFormat:]
+[NSPredicate predicateWithFormat:]
+[NSException raist:format:]
CFStringCreateWithFormat
CFStringAppendFormat
printf
os_log
比如刘能邀请赵四参加一个线上直播的session,刘能发出邀请后等待赵四接受邀请,赵四接受邀请之后告诉刘能,正常情况下时序图是这样的:
那刘能在发出邀请之后如果再发了一个接受邀请给赵四,是不是就可以强制双方建立联系了:
比如说我有一个StateManager用来根据通信内容管理用户的状态:
//StateManager.h
@interface StateManager : NSObject
- (void)handleMessage:(RemoteMessage *)message from:(NSString *)fromID;
@end
//StateManager.m
@interface StateManager ()
@property (nonatomic, strong) NSDictionary *sessions;
@end
@implementation StateManager
//这个函数根据传过来的message的类型做了不同的处理
- (void)handleMessage:(RemoteMessage *)message from:(NSString *)fromID {
switch (message.type) {
case RemoteMessageInvite:
[self handleSessionInviteWith:message from:fromID];
break;
case RemoteMessageInviteAccept:
[self handleSessionInviteAcceptedWith:message from:fromID];
break;
case RemoteMessageEnd:
[self handleMessageDisconnectedWith:message from:fromID];
break;
default:
break;
}
}
//当message.type是accept的时候会走到这个函数
- (void)handleSessionInviteAcceptedWith:(RemoteMessage *)message from:(NSString *)fromID {
Session *session = [self.sessions objectForKey:message.sessionIdentifier];
if (!session) {
return;
}
session.state = SessionStateConnected;
[session setupSocket];
}
@end
这么写有什么问题?message 是其他用户传过来的一个消息,这个是不可控、不可信的。会出现上面图中的情况,因此需要在获取 session 之后补充对 session 的校验:
if (session.state != SessionStateInviting) {
return;
}
此外,需要确保fromID是我们这个session中发出去的:
if (![session.invitedFromIDs containsObject:fromID]) {
return;
}
从这个例子中我们学到什么?
在实现一个方法的时候要明确这个类或者实例当中要满足什么样的条件(比如一些不等式)
通常这些不等式可能很微妙,容易出错
尤其要对那些你所不期待的或无法控制的参数格外敏感,提早发现问题,从而规避风险。
比如说我们经常遇到这样的场景:
服务端给了我们一些Json数据,本地解析成一个字典,然后在字典中添加一些本地的KV,这个字典用于更改UI或者数据库,那么数据流和安全墙大概是这个样子:
// Reaction.h
@interface Reaction : JSONModel
@property (nonatomic, strong) NSString *reactionType;
@property (nonatomic, strong) NSString *imageURL;
@property (nonatomic, strong) ReactionImageData *imageData;
@end
// MessageManager
- (void)didReceiveReactionData:(NSData *)data {
NSError *err = nil;
Reaction *reaction = [[Reaction alloc] initWithData:data error:&err];
if (reaction.imageData) {
NSURL *tmpURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:NSUUID.UUID.UUIDString];
[reaction.imageData writeToURL:tmpURL];
reaction.imageURL = tmpURL;
}
if (!err) {
[self.liveSreamVC displayReaction:reaction];
}
}
// LiveStreamController.m
- (void)displayReaction:(Reaction *)reaction {
// some implementation ...
NSURL *imageURL = reaction.imageURL;
if (imageURL) {
// some implementation ...
[[NSFileManager defaultManager] removeItemAtURL:imageURL error:nil];
}
}
这样写有什么问题?
加入传入的json已经包含了一个imageURL,那么可以用这样的代码删除沙盒中的任意文件!
{
"reaction_type": "thumbs-type"
"image_url": "file:///var/mobile/foo/database.db"
}
怎么样规避这种问题?
区分开可以信任的数据和不可信任的数据,把他们放到不同的对象当中,否则在使用这个对象的时候就会有歧义。
确保不可信任的数据做了充足的校验
但愿通过上面的一些基本概念和代码快,你可以知道怎么样对待安全问题。安全是一个很大的Topic,而且通常一个很小的代码错误会导致很大的问题,作为开发者,要时刻注意下面这几点:
定位到攻击者可以在哪里影响你的数据流
完善设计模式,从而可以减少风险
对于一些常见的Anti-Pattern要有足够的认知,知道如何处理
对于不可信任的数据,要格外注意
[1]
github issue: https://github.com/ZipArchive/ZipArchive/issues/453
程序员专栏 扫码关注填加客服 长按识别下方二维码进群
近期精彩内容推荐:
为何说IT科技公司应该留住35岁员工?
工友们!大家好,今天你摸鱼了吗?
缓存穿透,雪崩,击穿以及解决方案分析
图文详解:如何给女朋友解释什么是微服务?
在看点这里好文分享给更多人↓↓