Android系统内部构建了很多的安全特性,明显的减少了应用产生安全问题的频率。系统的安全是专门被设计过的,所以你可以使用系统默认的系统和文件权限来构建APP,避免了安全问题作出艰难的选择。安全的特性包括:
Android应用沙箱:将你的应用的数据和代码与其他应用分离
应用框架实现了稳健的安全功能,如密码、权限、安全的进程内通讯
类似ASLR\NC\ProPolice\safe_iop\OpenBSD dlmalloc\OpenBSD calloc\Linux_min_addr等技术的使用减轻了内存管理错误发生的危险。
加密的文件系统来避免数据丢失或者被盗
用户授权来限制系统访问和用户数据
应用自定义权限来控制每个应用的数据
对于Android系统的安全性有所了解非常重要,按照本文的要点规范你的代码可以很好的减少影响用户的安全问题的发生。
1. 数据存储:
使用内部存储空间:
默认的,你在内部存储空间创建的文件只能被你的应用访问,这是由Android系统保证的,试用于所有的应用。
对于进程内通讯(IPC)文件,你要避免使用MODE_WORLD_WRITEABLE或者MODE_WORLD_READABLE模式,否则将无法限制其他应用对其进行访问,也不提供数据格式的控制。如果你想与其他进程共享你的数据,你需要考虑使用content provider,这可以针对其他应用提供读或者写的权限并且可以动态控制权限的分配。
对于敏感的数据提供额外的保护,你可以使用一个应用不可以直接访问的key来加密本地文件。如key可以被保存在KeyStore内并且需要不存储在本地设备的用户密码来访问。这不提供可以监视用户输入密码的root的数据保护,但是包括了丢失设备时文件又没有加密的场景。
使用外部存储空间:
文件也可以存储在如SD卡类的外部存储上,这会是全局可以读写的。因为用户也可以删除外部存储的文件、其他应用也可以修改,所有不要将敏感信息的文件存储在外部存储空间内。
因为数据可能是不可信任的来源,你在处理外部存储空间的数据时一定要执行输入的安全校验。强烈建议不要将可执行的类或者文件存储在外部存储空间并动态加载。如果你的应用确实要读取外部存储空间的可执行文件,文件一定要签名和加密校验。
使用Content providers:
Content providers提供了一个机构化的机制来限制你的应用或者其他应用对数据的访问。如果你不希望其他应用访问数据,可以设置android:exported=false到manifest文件。
当创建一个可以对其他应用开发的ContenProvier时,你可以特殊的设置一个读写权限(也可以分别设置两个权限)。永远记住,先设置权限然后暴露功能是容易的,破话权限并影响存在的用户是困难的。
如果你的Content proviers只是在你自己的应用内共享数据,建议你使用android:protectionLevel设置为signature。Signture权限不需要用户的确认,提供了更好的用户体验,并且更好的实现了对数据的访问控制。
Content providers还可以声明android:grantUriPermissions来实现更细粒度的访问控制,还需要在激活的Intent对象内使用FLAG_GRANT_READ_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSION标签。这些权限的访问可以使用<grant-uri-permissionelement>来更好的控制。
当使用参数化的查询方法(query()\update()\delete())访问content providers时,要避免从不可信任的来源引入潜在的SQL注入安全问题。
不要对写权限有错误的认识。考虑到写权限允许使用SQL语句,这样就导致可以在语句中通过WHERE条件并检查结果实现一些数据的确认。如攻击者可以一直尝试写数据where一个电话号码存在,检测结果来探测数据的存在性。如果content provier有了一个可预测的结果,写权限可能基本等同与读和写的权限。
2. 使用权限
申请权限:
建议最小化你的应用申请的权限,减少对敏感权限的访问可以降低错误使用的风险、易于用户安装操作、降低被攻击的脆弱性。不需要使用权限不要申请。
优先选择不在你的应用里不使用权限的方案,如优先使用内部存储数据而不是使用需要申请权限的外部存储。
你的应用可以使用<permissions>来保护安全敏感的、开发给其他应用的进程内交互数据。一般建议使用访问控制而不是需要用户确认的权限控制,太多的权限会使用户混淆,如使用签名提到的签名级别的保护,这对用户来说是透明的,并且实现了权限的校验。
创建权限:
通常来讲,在满足安全需求的同时你需要努力减少权限的定义。新创建的权限对于大多数应用来说是不通用的,系统定义的权限基本已经覆盖了大多数的场景。
如果你需要创建一个新的权限,可以考虑能否使用签名级别的保护实现。
如果你创建了一个dangerous级别的保护权限,有几个复杂的事情需要考虑到:权限要有一个字符串对应简明的描述安全决策、描述字符串要是国际化的、用户可能选择不再安装你的应用、应用申请的权限可能面临权限创建没有被安装。
3. 使用网络
网络处理对于安全是固定风险的,因为它发送的数据可能是用户私有的。用户越来越多的关注移动设备的隐私,尤其是设备需要联网的时候,所以你的应用尽最大可能实现安全约定以保证用户的隐私安全。
使用IP网络:
Android上的网络和Linux环境上的没啥不同,要考的最关键的地方是保证使用合适的协议传输敏感数据。如使用HttpsURLConnection来传输数据更安全,我们更倾向于使用HTTP上的HTTPS,因为移动设备经常连接的网络可能是不安全的,如公共的Wi-Fi网络。
通过SSLSocket类可以轻松的实现认证和socket层的加密通讯,由于用户频繁的使用Wi-Fi链接不安全的网络,你的应用强烈建议使用安全的网络交互。
有时应用使用本地网络端口来处理敏感的进程内通讯,不建议使用这种方式,因为接口可能被设备上的其他应用访问。替代的,应该使用Andoird提供的需要认证的进程内通讯机制,如Service。
另一个场景注意是不要相信通过HTTP或者其他不安全的协议下载的数据,包括了用户在WebView输入的数据和HTTP请求的返回数据。
使用移动网络:
由于SMS短信是主要设计用于用户与用户间的通讯,所以不适合应用之间传输数据。官方建议使用Google Cloud Messaging和IP网络来在服务器和应用设备间传递数据消息。
SMS在设备上和网络上是既不加密又不认证的,尤其受到的短信可能是别人恶意发送的。不要依赖未认证的SMS数据来执行敏感的命令,SMS在网络上有可能会被拦截。Android本身上SMS信息会作为广播intent,这可能被其他应用接收到并读取。
4. 执行输入校验
输入参数校验不全是影响应用最普通的安全问题,不论是什么样的平台。Android提供了一些平台级别的对策来减少输入校验问题的产生,这些特性要尽可能使用。此外,选择一个类型安全的语言可以减少参数校验问题的发生。
如果你使用本地代码,那么从文件读取的、从网络读的、从近处内通讯读的数据都可能产生安全问题,最常见的问题时缓存溢出、释放后使用。Android提供了几个技术(ASLR/DEP)可以减少这些错误的发生,但是它们解决不了底层的问题,通过仔细的处理指针和管理缓存你可以解决避免问题。
如果你使用的数据需要在查询作为参数拼接到SQL里来查询数据库或者Content provider,SQL注入会成为安全问题。最好的方式是使用参数化的查询方式查询content provider而不是自己拼接。限制只读和只写权限同样可以减少SQL拦截的风险。
如果你无法使用上面的安全特性,强烈建议使用合理的结构化数据形式,然后校验数据格式看是否是预期的。同时非法字符黑名单和字符替换也是很好的策略。
5. 处理用户数据
通常,保证用户数据安全的最好方法是最少的使用API接口访问有用户敏感或者私人数据。如果你已经访问了用户数据并且能避免存储或者发送这些数据,不要存储或者发送它们。最后,考虑你的应用逻辑是否可以使用哈希或者不可逆的数据,如你的应用使用eamil的哈希值作为主键,避免存储和发送email。这些避免了不经意间曝光数据,也减少了黑客工具你的应用的可能。
如果你的应用访问用户名和密码类的私人信息,记住司法权可能需要你提供你使用和存储隐私信息的策略。按照安全要求最小化的访问数据会简化规定实现。
你需要考虑你的应用是否会不经意间曝光用户信息到三方应用,如你的应用使用的三方广告组件、三方服务。如果你不知道为什么一个组件或者服务需要个人信息,不要提供。通常你的应用减少个人信息的访问也会减少这种问题的发生。
如果你的应用访问敏感数据是需要的,评估下数据是否一定要发送到服务器,是否可以在客户端完成。考虑使用敏感数据的代码在客户端执行而不是提交到服务器。
保证你不会不经意间曝光用户数据给设备上的其他应用,如通过IPC、可写文件、网络端口。这也是前面建议减少权限来保护数据的原因。
如果需要GUID,创建一个大的独立的数字存储它。不要使用手机号之类涉及用户信息的最为标识。
写日志时也要小心,日志是共享资源,可以被有读日志权限的所有应用读取。及时日志数据是临时的或者重启时可擦除,不恰当的日志会立刻泄露用户信息给其他应用。
6. 使用WebView
因为WebView可以解析处理web内容,包括HTML和JavaScript,不恰当的用户可能会产生常见的web安全问题,如cross-site-scripting(JavaScript injection,XSS),Android包括了几个机制来减少那些潜在问题的范围,通过限制WebView的能力到最小功能实现。
如果你的应用不在WebView里直接使用JavaScript,不要调用setJavaScriptEnabled()。JavaScript不能执行直接避免了cross-site-scripting的可能。
要小心使用addJavaScriptInterface(),这会使JavaScript可以调用通常涉及给Android应用的操作接口。如果你需要使用,只能暴露给输入可信任的web页面。通常,我们只建议曝光的addJavaScriptInterface()只能使用在你的应用里包含的JavaScript内。
如果你的应用使用WebView访问敏感数据,记住使用clearCache()方法清理本地缓存的文件。服务器端也尽量使用no-cache的方式,应用不能缓存一些特殊的内容。
4.4以前的Android版本使用的webkit核心有很多的安全问题。作为一个解决方案,运行这些老版本的设备里要保证只有WebView展示可以信任的内容,你需要使用一个可更新的安全提供对象已保证你的应用不暴露在潜在的SSL安全漏洞里。如果你的应用必须从开发的web渲染内容,考虑提供你自己的渲染机制来保证它与安全补丁一起更新。
7. 处理用户凭证
通常建议最小化要求用户凭证的频率,不然容易导致钓鱼攻击更加突出。替代的使用认证token并刷新token。
如果可能的话,用户名和密码不要存储在用户设备上。替代的,只用用户名和密码执行起始时的认证,然后使用短期有效的、服务器定制的认证token。
可以被多个应用访问的服务要使用AccountManager访问,如果可能的话使用AccountManager类来调用云服务而不是存储密码在用户设备上。
如果只有你创建的应用不需要凭证而大多数应用需要,你可以使用checkSignature()来校验访问AccountManager的应用。如果只有一个应用使用凭证,你可以使用KeyStore存储。
8. 使用密码
处理提供数据隔离,Android提供了广泛的使用密码保护数据的算法,如支持全文件系统加密,提供安全通行通道。
通常来讲,使用最高级别的已经存在框架实现可以支持你的需要。如你需要从一个位置安全的接收文件,一个简单的HTTPS的URI地址就足够满足而不需要使用密码。如果你需要一个安全的通道,考虑使用HttpsURLConnection或者SSLSocket,总比你实现自己的协议容易。
如果你确实需要实现你自己的协议,我们强烈建议你不要实现你自己的密码算法。使用存在的密码算法,如AES或者RSA,在Cipher类里都有提供。
使用一个安全的随机的生成器(SecureRandom)来生成任何的密码keys,使用不安全的会因算法薄弱而受到黑客攻击。
如果你需要存储一个key以备重复使用,使用KeyStore提供的机制,可以长时间存储并且可以重复浏览加密keys。
9. 使用进程内通讯
一些应用尝试使用Linux技术实现自己的进程内通讯,如网络socket或者共享文件。我们强烈建议使用Android系统自带的技术,如Intent/Binder/Messager/Service/BroadcastReceiver。Android的进程内通讯允许你校验链接IPC应用的身份并且设置安全策略应用到IPC。
使用Intent:
Intent是Android提供的优先使用的异步IPC机制,根据你的应用的需求,可以使用sendBroadcast()或者sendOrderedBroadcast(),或者一个明确的intent来特别定义应用的组件。
注意顺序的广播broadcast可以被接收器消费掉,所以它可能不会被送达到所有应用。如果你正在发送一个intent到一个特别的receiver,你需要使用通过名称生命receiver的明确的intent。
Intent的Senders可以在方法调用时校验接收器是否有一个非空的权限,只有有权限的应用才会接收到intent。如果broadcast广播的Intent有敏感数据,你需要考虑使用权限来保证恶意的应用不会注册并接受到消息。在那些情况下,你可以考虑直接调用接受者而不是通过broadcast。Intent的filters并不是一个安全特性,你需要在你的intent的receiver里执行输入参数的校验来保证安全。
使用Services:
一个Service通常是用来给其他应用提供功能。每一个service必须在manifest文件里有一相关的声明。
默认的,service是不能够导出的不能够被其他应用调用。然而,如果你在service声明里添加了任何的intent filters,这就是默认导出的。你最好是明确声明android:exported来确定你希望的行为。Service也可以使用android:permission属性保护,其他应用必须声明相应的<users-permission>元素到manifest来实现服务的启停绑定。
服务也可以通过调用checkCallingPermission()来实现独立的IPC调用权限,但是通常建议使用manifest声明的方式,有于监视查询。
使用binder和messager消息接口:
使用Binder和Messager是Android里优先使用的RPC类型的IPC技术,它们是被良好设计的接口,如果需要可以允许端点彼此认证。
我们强烈建议设计一个不需要接口权限校验的接口,Binder和Messager对象不需要在manifest里声明,因此不能直接声明权限,它们通常继承它们实现的Service和Activity的权限。如果你正在创建一个接口需要认证和访问控制,这些控制需要通过代码方式添加到Binder和Messager接口里。
如果正在提供一个需要访问控制的接口,使用checkCallingPermission()来校验调用者是否有权限。如果调用Service的接口,bindService调用可能会因为没有权限而失败。如果调用本地的你自己应用的接口,可以使用clearCallingIdentity()来满足内部安全检查。
使用广播接收器broadcast receivers:
默认的,receiver是导出的可以被任何应用调用,可以声明一个安全权限做控制。
10. 动态加载代码
强烈建议不要从应用APK的外部加载代码,这增加了代码注入和代码篡改的可能,同样增加了版本管理和应用测试的复杂度。最后还会导致校验应用行为成为不可能,在一些环境里会被禁止。
如果确实需要动态加载代码,最重要的是记住动态加载的代码是和应用APK具有相同的权限的。
动态加载代码最主要的安全风险是代码需要来自可验证的来源,如果组件是包括在你的APK里,他们是不能被其他应用修改的,不论代码是本地库还是DexClassLoader加载的类。有一些应用尝试从不安全的地方加载代码,如为加密协议的网络、可别修改的外部存储空间,这都可能导致数据是被修改过的。
11. 虚拟机安全
Dalvik是Android的运行时虚拟机,专门针对Android设计的,但是很多的使用于其他虚拟机的安全代码也被用到了Android上。对于其他环境的虚拟机的开发经验,相对Android虚拟机有两个主要的不同:
诸如JVM、.net之类虚拟机,运行起来好像是一个安全的边界,从底层操作系统能力隔离出了代码。在Android上,Dalvik虚拟机不是安全的边界,应用的沙箱是通过操作系统层面实现的,应用里的Dalvik可以不需要任何约束的与本地代码交互。
移动设备是限制存储空间的,开发人员通常需要动态加载代码。在Android是不建议使用的。从网络或者外部存储空间加载的代码可能存在被篡改的风险。
12. 本地代码的安全
通常建议尽可能的使用Android的SDK而不自己写本地代码。使用本地代码会使应用更复杂、兼容性更差、更可能导致缓冲器溢出之类的内存问题。
Android是使用Linux内核构建的,如果需要编写本地代码你需要对Linux开发安全实践有所熟悉。Android与大多数Linux环境的不同的是应用沙箱。Android的所有应用是运行在应用沙箱里的,包括本地的代码,并且每个应用是有一个唯一的UID和相应的受控权限的。