背景
安全风险也是一种bug,是一个比较大的topic,通常比较难发现,但会产生比较严重的影响。本次技术分享意图通过一些基本的概念和例子来初步探讨怎么样在开发过程中规避风险。
基本哲学-Threat Modeling(威胁建模,TM)
「威胁建模(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通信(如视频通话)
- 蓝牙传输

一些常见的Anti-Patterns
在理清什么样的数据不可信任之后,我们列举出一些典型的错误代表,看看开发过程中经常遇到的错误。
Path Traversal 路径遍历 (比如iOS广泛使用的SSZipArchive, github issue)
比如在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"]
;
背景
安全风险也是一种bug,是一个比较大的topic,通常比较难发现,但会产生比较严重的影响。本次技术分享意图通过一些基本的概念和例子来初步探讨怎么样在开发过程中规避风险。
基本哲学-Threat Modeling(威胁建模,TM)
「威胁建模(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通信(如视频通话)
- 蓝牙传输

一些常见的Anti-Patterns
在理清什么样的数据不可信任之后,我们列举出一些典型的错误代表,看看开发过程中经常遇到的错误。
Path Traversal 路径遍历 (比如iOS广泛使用的SSZipArchive, github issue)
比如在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"]
;
背景
安全风险也是一种bug,是一个比较大的topic,通常比较难发现,但会产生比较严重的影响。本次技术分享意图通过一些基本的概念和例子来初步探讨怎么样在开发过程中规避风险。
基本哲学-Threat Modeling(威胁建模,TM)
「威胁建模(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通信(如视频通话)
- 蓝牙传输

一些常见的Anti-Patterns
在理清什么样的数据不可信任之后,我们列举出一些典型的错误代表,看看开发过程中经常遇到的错误。
Path Traversal 路径遍历 (比如iOS广泛使用的SSZipArchive, github issue)
比如在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"]
;