Android应用安全之Content Provider安全
android平台提供了Content Provider,将一个应用程序的指定数据集提供给其它应用程序。这些数据可以存储在文件系统、SQLite数据库中,或以任何其它合理的方式存储。其他应用可以通过ContentResolver类从该内容提供者中获取或存入数据。
Content Provider通过URI(统一资源定位符)来访问数据,URI可以理解为访问数据的唯一地址。
限制app对敏感Content Provider的访问
Content Provider类提供了一种机制用来管理以及与其它应用程序共享数据。在与其它应用程序共享Content Provider的数据时,应该实现访问控制,禁止对敏感数据未经授权的访问。
有三种方法来限制对内容提供者的访问:
- 公开的
- 私有的
- 内部的
公开的
通过在AndroidManifest.xml文件中指定android:exported属性为true,可以设置将Content Provider公开给其它应用程序。对于API Level低于17的Android应用程序,Content Provider默认是public的,除非显式指定android:exported="false"。例如:
<provider android:exported="true" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />
如果一个Content Provider为公开的,Content Provider中存储的数据就可以被其它应用程序访问。因此,它只能用于处理非敏感信息。
私有的
通过在AndroidManifest. xml文件中指定android:exported属性为true,可以设置将Content Provider设置为私有的。从API Level17及以后,Content Provider默认是私有的,除非显式指定android:exported="true"。例如:
<provider android:exported="false" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />
开发建议
- 如果不需要与其他应用程序进行数据共享,就应该在manifest文件中设置android:exported="false"。
- 但是值得注意的是,在API Level低于8时,即使显式地声明了android:exported="false",其它应用程序仍然可以访问你的Content Provider。
- 需要向外部提供数据的content provider则需设置访问权限,如:
下面的元素请求对用户词典的读权限:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
申请某些protectionLevel="dangerous"的权限:
<uses-permission android:name="com.huawei.dbank.v7.provider.DBank.READ_DATABASE"/>
<permission android:name="com.huawei.dbank.v7.provider.DBank.READ_DATABASE" android:protectionLevel="dangerous"></permission>
防止SQL注入
数据查询
传递给ContentProvider的参数应该被视为不可信的,不能直接用于sql查询。
一个程序要访问暴露的provider,首先要知道访问的目标地址,类似http协议,provider也有自己的规范,即类似content://com.aaaa.bbb.class/tablename
其中,com.aaaa.bbb为包名,class为类名,tablename为表名,一般是这样子,具体看自己定义了。
看一个查询例子:
Cursor cursor = contentResolver.query(
Words_CONTENT_URI, new String[]{"user","pwd"},
null, null, null);
这是调用contentResolver的query方法进行数据库查询,返回一个cursor对象,即类似DataReader的东西,里面是返回结果。
来看看query的参数
Cursor android.content.ContentResolver.query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
uri 即content://com.aaaa.bbb.class/tablename
projection 即你要查询的列名
selection 和 selectionArgs 共同控制后面的sql语句中的where内容.
sortOrder 即order by xxx的内容。
那么例子中的查询整个构造的语句即:
select user,pwd from tablename;
2.Sql注入问题
综合上面的内容,我们可以看到,query里至少两个参数我们可控,一个是projection,一个是selection,这两个都会影响SQL与的组成,也就为注入提供了可能。
这里以某app为例,该app对外暴露了一个content provider,uri为:content://com.masaike.mobile.downloads/my_downloads,其中com.masaike.mobile为包名,downloads为库的名字,my_downloads为表名(不一定,可自定义的哦)。
现在语句这么写:
Cursor cursor = contentResolver.query("content://com.masaike.mobile.downloads/my_downloads", new String[]{"_id'","method"},null, null, null);
其中_id和method为两个字段名,我们在_id后面加个单引号,运行下看logcat内容:

从上图很容易看出来,SQL语句因为有个单引号,导致出错。所以注入是存在的。
而如果我们修改projection的内容为"* from sqlite_master where type='table';--",这样子即在闭合后面查询的情况下显示出来全部的表名。当然也可以构造其他语句了。
(参考cnrstar http://lcx.cc/?i=4462)
开发建议
1.传递给ContentProvider的参数应该被视为不可信的输入,不应该在没有经过任何处理的情况下直接用于SQL查询。如果查询语句中包含SQL代码则可以返回数据或者允许攻击者未授权地访问应用数据库的数据。
2.使用ContentProvider提供外部应用程序进行数据库存取时应使用带占位符的参数化查询防SQL注入。
3.SQLiteDatabase对象的部分内置方法是可以有效防SQL注入的,比如query(),insert(),update和delete(),另外,正确地使用rawQuery()也可以防止SQL注入,而execSQL()方法建议避免使用。
(1)、使用SQLiteDatabase对象自带的防SQL注入的方法,比如query(),insert(),update和delete():
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB"); SQLiteDatabase db = dbhelper.getReadableDatabase();
/*查询操作,userInputID和userInputName是用户可控制的输入数据 */
Cursor cur = db.query("user", new String[]{"id","name"}, "id=? and name=?", new String[]{userInputID,userInputName}, null, null, null);
/* 插入记录操作*/
ContentValues val = new ContentValues();
val.put("id", userInputID);
val.put("name", userInputName);
db.insert("user", null, val);
/*更新记录操作*/
ContentValues val = new ContentValues();
val.put("id", userInputName);
db.update("user", val, "id=?", new String[]{userInputID });
/*删除记录操作*/
db.delete("user", "id=? and name=?", new String[]{userInputID , userInputName });
(2)、正确地使用SQLiteDatabase对象的rawQuery()方法(仅以查询为例):
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB"); SQLiteDatabase db = dbhelper.getReadableDatabase();
/* userInputID和userInputName是用户可控制的输入数据*/
String[] selectionArgs = new String[]{userInputID , userInputName };
String sql = "select * from user where id=? and name=?";//正确!此处绑定变量
Cursor curALL = db.rawQuery(sql, selectionArgs);
(3)、以下为错误案例!仅供参考:
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB"); SQLiteDatabase db = dbhelper.getReadableDatabase();
/*案例1:错误地使用rawQuery(),未绑定参数*/
String sql = "select * from user where id=‘" + userInputID +"‘";//错误!动态拼接,未绑定变量 Cursor curALL = db.rawQuery(sql, null);
/*案例2:使用execSQL()方法*/
String sql = "INSERT INTO user values(‘"+userInputID +"‘,‘"+userInputName +"‘)";//错误同上 db.execSQL(sql);
4.ContentProvider支持对指定的Uri分别设置读权限和写权限,建议只开放能完成任务的最小权限。
(参考http://shikezhi.com/html/2015/android_0819/134898.html)
规范ContentProvider的url
没有正确的覆写openFile方法,导致攻击者可以通过更改访问目录,遍历系统中所有文件。
通过使用ContentProvider.openFile()方法,可以方便其它应用访问你的应用程序数据。根据ContentProvider的实现方式,使用openFile方法可以导致目录遍历漏洞。因此,当通过ContentProvider交换文件的时候,文件路径在使用之前应该被规范化。
不合规代码Example 1
在这个不合规代码示例中,试图通过调用android.net.Uri.getLastPathSegment()获取paramUri路径的最后一段,即文件名称。此文件存在于预先配置的父目录IMAGE_DIRECTORY中。
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
throws FileNotFoundException {
File file = new File(IMAGE_DIRECTORY, paramUri.getLastPathSegment());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
然而,当这个文件路径被URL编码后,就意味着被访问的这个文件可能会存在于预配置的父目录之外的一个不可预知的目录中。
从Android 4.3.0_r2.2开始, Uri.getLastPathSegment()方法在内部调用了Uri.getPathSegments():
public String getLastPathSegment() {
// TODO: If we haven't parsed all of the segments already, just
// grab the last one directly so we only allocate one string.
List<String> segments = getPathSegments();
int size = segments.size();
if (size == 0) {
return null;
}
return segments.get(size - 1);
}
Uri.getPathSegments()方法的部分代码如下:
PathSegments getPathSegments() {
if (pathSegments != null) {
return pathSegments;
}
String path = getEncoded();
if (path == null) {
return pathSegments = PathSegments.EMPTY;
}
PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();
int previous = 0;
int current;
while ((current = path.indexOf('/', previous)) > -1) {
// This check keeps us from adding a segment if the path starts
// '/' and an empty segment for "//".
if (previous < current) {
String decodedSegment = decode(path.substring(previous, current));
segmentBuilder.add(decodedSegment);
}
previous = current + 1;
}
// Add in the final path segment.
if (previous < path.length()) {
segmentBuilder.add(decode(path.substring(previous)));
}
return pathSegments = segmentBuilder.build();
}
Uri.getPathSegments() 方法首先通过调用getEncoded()获取了文件路径,然后使用"/"作为分隔符将路径分割成几部分,任何被编码的部分都将通过decode()方法进行URL解码。
如果文件路径被URL编码,那么分隔符就变成了"%2F",而不再是"/",getLastPathSegment()就可能不会正确地返回路径的最后一段,从而导致目录遍历漏洞的产生。
如果Uri.getPathSegments()在进行路径分割之前对路径进行解码,那么经过URL编码的路径就会被正确地处理,可惜没有这么实现。
不合规代码Example 2
在本不合规代码示例中,试图通过调用Uri.getLastPathSegment()两次来修复第一个不合规代码示例中的漏洞。第一个调用意在进行URL解码,第二个调用是用于获取开发人员需要的字符串。
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
throws FileNotFoundException {
File file = new File(IMAGE_DIRECTORY, Uri.parse(paramUri.getLastPathSegment()).getLastPathSegment());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
例如,当以下的URL编码字符串传递给content provider后会发生什么情况呢?
..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第一次调用Uri.getLastPathSegment()函数会返回以下字符串:
../../../data/data/com.example.android.app/shared_prefs/Example.xml
字符串通过Uri.parse()转换成了Uri对象, 然后作为第二次调用Uri.getLastPathSegment()函数的参数。得到的结果如下:
Example.xml
这个字符串是用来创建一个文件对象的。然而,如果攻击者提供了一个特殊的字符串,该字符串在第一次调用Uri.getLastPathSegment()时不能被解码,那么就获取不到分割路径的最后一段。这样的字符串可以使用双重编码技术创建:
双重编码
例如,下述双重编码字符串就能绕过该漏洞修复:
%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml
第一次调用Uri.getLastPathSegment() 会将 "%25" 解码为"%",得到如下字符串:
%2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
当把这个字符串传递给第二次调用的 Uri.getLastPathSegment()时,"%2E"和"%2F" 就会被解码,得到如下字符串:
../../../data/data/com.example.android.app/shared_prefs/Example.xml
从而导致目录遍历的可能。
仅仅通过解码字符串来防止示例中的目录遍历攻击是不够的,还必须检查解码后的路径是否在目标目录下。
PoC
以下恶意代码可对第一个不合规代码示例中的漏洞进行利用:
String target = "content://com.example.android.sdk.imageprovider/data/" + "..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml"; ContentResolver cr = this.getContentResolver(); FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte[] buff = new byte[fis.available()]; in.read(buff);
PoC (Double Encoding)
以下恶意代码可对第二个不合规代码示例中的漏洞进行利用:
String target = "content://com.example.android.sdk.imageprovider/data/" + "%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml"; ContentResolver cr = this.getContentResolver(); FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte[] buff = new byte[fis.available()]; in.read(buff);
解决方案
在下述解决方案中,在使用文件路径前通过Uri.decode()对其进行了解码。同样的,在文件对象创建后,通过调用File.getCanonicalPath()将路径进行了规范,同时检查它是否存在于IMAGE_DIRECTORY目录中。
通过使用规范化后的路径,即使文件路径被双重编码,目录遍历漏洞也可以得到缓解。
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
throws FileNotFoundException {
String decodedUriString = Uri.decode(paramUri.toString());
File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment());
if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
开发建议
ContentProvider.openFile()方法提供了一种方便其它应用程序访问自己的数据(文件)的方式,但是使用这个方法会导致一个目录遍历漏洞。因此,当通过ContentProvider访问一个文件的时候,路径应该被规范化。
1.使用ContentProvider.openFile()之前需要调用Uri.decode()
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri, String paramString)
throws FileNotFoundException {
String decodedUriString = Uri.decode(paramUri.toString());
File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment());
if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {
throw new IllegalArgumentException();
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
2.设置exported=“false”
3.设置恰当的访问权限
附:
使用adb命令测试content provider的方法
usage: adb shell content query --uri <URI> [--user <USER_ID>] [--projection <PROJECTION>] [--where <WHERE>] [--sort <SORT_ORDER>] <PROJECTION> is a list of colon separated column names and is formatted: <COLUMN_NAME>[:<COLUMN_NAME>...] <SORT_OREDER> is the order in which rows in the result should be sorted. Example: # Select "name" and "value" columns from secure settings where "name" is equal to "new_setting" and sort the result by name in ascending order. adb shell content query --uri content://settings/secure --projection name:value --where "name=\'new_setting\'" --sort "name ASC"
Android应用安全之Content Provider安全的更多相关文章
- Android应用程序组件Content Provider的共享数据更新通知机制分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6985171 在Android系统中,应用程序组 ...
- Android应用程序组件Content Provider在应用程序之间共享数据的原理分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6967204 在Android系统中,不同的应用 ...
- Android应用程序组件Content Provider的启动过程源代码分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Andr ...
- Android应用程序组件Content Provider应用实例
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6950440 文简要介绍了Android应用程序 ...
- Android应用程序组件Content Provider简要介绍和学习计划
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6946067 在Android系统中,Conte ...
- Android学习五:Content Provider 使用
1ContentProvider相关知识1.1在安卓应用中,通过文件方式对外共享数据,需要进行文件操作读写数据:采用sharedpreferences共享数据,需要使用sharedpreference ...
- Android开发-API指南-Content Provider基础
Content Provider Basics 英文原文:http://developer.android.com/guide/topics/providers/content-provider-ba ...
- Android开发-API指南-Content Provider
Content Providers 英文原文:http://developer.android.com/guide/topics/providers/content-providers.html 采集 ...
- Android跨进程通信Content Provider
Content Provider ContentProvider在android中的作用是对外共享数据,也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通 ...
随机推荐
- paip.提升性能---list,arraylist,vector,linkedlist,map的选用..
paip.提升性能---list,arraylist,vector,linkedlist,map的选用.. arraylist,vector基本一样,但是,vector线程安全的. 作者Attilax ...
- 更新日志 - fir.im 新版管理后台邀请内测
上周,我们对fir.im 新版管理后台的页面结构和样式进行了优化,现在新版的管理后台开始邀请内测,感兴趣的伙伴可以发邮件到 **beta@fir.im** 申请.为了保证服务质量和对问题进行有效追踪, ...
- iOS7初体验(3)——图像资源Images Assets
开始之前,首先回顾一下iOS7初体验(1)——第一个应用程序HelloWorld中的一张图,如下所示: 本文便分享一下Images.xcassets的体验~_~ 1. 打开此前使用过的HelloWor ...
- dao层
/* * 查询 */ public List<User> selectAll(); public User findById(String id); public User findByU ...
- forword/ sendRediect
res.sendRedirect(),是重定向,相当于两次请求,两次相应,地址栏会发生变化. 在实际使用中,重定向不能传指.也就是在requset中储存的值在跳转到另外一个页面后,在目标页面提取不出来 ...
- gulp学习笔记2
gulp系列学习笔记: 1.gulp学习笔记1 2.gulp学习笔记2 3.gulp学习笔记3 4.gulp学习笔记4 1. 压缩 CSS 压缩 css 代码可降低 css 文件大小,提高页面打开速度 ...
- 取消 virtualStore 注册表[启用和禁止 UAC虚拟化]
近日发现,在win2008R2 x64下运行的服务器程序,其注册表读取路径为: [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\SZDomain\itvc1] 但是经 ...
- Windows系统补丁KB2962872导致InstallShield无法启动(解决方案已更新)
20140717最新更新: Flexera Software发布了临时补丁包,该补丁包暂时禁止了InstallShield Trialware功能(中国区用户很少有用此功能)两种安装方法: 方法1. ...
- HOWTO - Basic MSI安装包在安装运行过程中如何获取完整源路径
有朋友问到如何在一个Windows Installer安装包中获取安装包源路径,就是在安装包运行过程中动态获取*.msi所在完整路径. 这个问题分两类,如果我们的安装包只是一个*.msi安装文件,那么 ...
- 【Vegas原创】SVN的搭建及安装使用
中文手册:http://tortoisesvn.net/docs/nightly/TortoiseSVN_zh_CN/index.html 所需程序: 1,TortoiseSVN http://so ...