引言
在 Android 开发中,不同应用之间共享文件是一项非常常见的需求,例如:
-
相机 App 把拍好的照片传给社交 App
-
文件管理 App 把文件交给 PDF 查看器
-
App 把图片提供给微信、钉钉等应用进行分享
如果通过 file:// URI 或放宽文件系统权限的方式来共享文件,极容易造成 权限越界、隐私泄露甚至攻击链扩展风险。
本文将从风险根源、错误示例、攻击场景、正确用法三部分结构化解释 Android 官方推荐的安全方案 FileProvider。
一、文件共享场景
FileProvider 是 Android 官方提供的、用于在应用之间安全共享文件的机制,适用于:
- 两个 App 之间共享文件
- 需要跨应用发送图片、视频、日志、文档
- 给微信、钉钉、邮箱等分享资源
- 读取自身私有目录文件时避免直接暴露路径
二、风险根源:为什么 file:// 是灾难
如果通过 file:// 共享文件,本质上是直接暴露真实的文件路径:
file://data/data/com.example.demo/files/images/abc.log
file:///sdcard/DCIM/Camera/xxx.jpg
这会带来几类严重风险:
1. 暴露真实目录结构(敏感)
对方 App 获得你的真实目录,如:
-
包名
-
模块名
-
私有目录结构
-
文件命名方式
这些信息在攻击链里非常有价值。
2. 允许遍历 sdcard 目录(最高风险)
如果分享的是:file:///sdcard/xxx/abc.jpg
那么任何三方应用都可以遍历整个 SD 卡:
-
私密照片
-
日志文件
-
应用缓存
-
下载文件
-
录音、文档
这正是 Android 7.0+ 禁止 file:// 的核心安全原因。
3. 私有目录虽读不了,但依然泄露信息(中风险)
例如:file://data/data/com.example.demo/files/images/abc.log
对方虽然不能读该文件,但:
-
暴露了包名结构
-
暴露了模块路径
-
在高权限(root/提权)环境中可直接用于攻击利用
仍属隐私泄露和攻击辅助信息。
4. Android 7.0+ 直接崩溃
Android N 引入了严格限制,给 file:// 触发:
FileUriExposedException
这是系统明确禁止 file:// 的信号。
三、错误示例(file:// 导致的安全问题)
下面是分享app典型错误代码:
// ❌ 错误示例:直接使用 file:// URI 共享文件(Android 7.0+ 会崩溃)
//filePath就是要分享出去的文件路径,如/data/data/com.example.demo/files/images/abc.log
void shareImageInsecurely(String filePath) {
// 用传入的文件路径创建 file 对象
File picture = new File(filePath);
// 构建分享 Intent
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/jpeg");
// ⚠ 这里生成的是 file:// 协议的 URI,而不是安全的 content://
Uri uri = Uri.fromFile(picture);
intent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(intent, "Share Image"));
}
对方 App 实际接收到的内容:
Intent {
act=android.intent.action.SEND
typ=image/jpeg
clipData=ClipData { image/jpeg U:file://data/data/com.example.demo/files/images/abc.log }
extras={
android.intent.extra.STREAM=file://data/data/com.example.demo/files/images/abc.log
}
}
这意味着:对方已经知道你的真实目录结构
攻击思路因路径不同而不同:
| 分享路径 | 风险级别 | 原因 |
|---|---|---|
| sdcard | 🚨 极高 | 对方App可根据目录信息遍历整个 SD 卡 |
| data/data | ⚠ 中等 | 对方读不到本应用的私有目录信息,但暴露结构,可用于攻击链辅助 |
四、正确方案:使用 FileProvider 隐藏真实路径
FileProvider 使用虚拟化的 content:// URI:content://com.example.myapp.fileprovider/log_images/abc.log
接收方无法知道真实路径,只能访问被许可的单个文件。下面是安全分享私有文件/data/data/com.example.demo/files/images/abc.log的示例:
1️⃣ Manifest:注册 FileProvider
<!-- AndroidManifest.xml -->
<!-- FileProvider 配置 -->
<provider
//name的值为固定写法,是google官方实现的,共用这个名字
android:name="androidx.core.content.FileProvider"
//后面java代码要用与authrities同一字符串
android:authorities="com.example.myapp.fileprovider"
//该provider默认不对外暴露
android:exported="false"
//允许系统把这个 Provider 的 Uri 临时授权给其他应用,如果此处设置为false,即便在java代码中将这个provider下的文件分享出去,对方也没有权限。
android:grantUriPermissions="true">
<!-- 声明可暴露的路径白名单 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
关键点:
- exported=false → 默认不对外
- grantUriPermissions=true → 可临时授权给目标 App
2️⃣ FileProvider 路径白名单
res/xml/file_paths.xml
<!-- res/xml/file_paths.xml -->
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--
files-path 表示 /data/data/<包名>/files/ 目录
这里我们只开放 files/images/ 这个子目录
最终可访问的真实路径:/data/data/com.example.myapp/files/images/
-->
<files-path
name="log_images"
path="images/" />
</paths>
这样,FileProvider 只允许访问 files/images/ 下的文件。
虚拟路径会变成:
content://com.example.myapp.fileprovider/log_images/abc.log
3️⃣ Java:通过 FileProvider 分享文件
// MainActivity.java
/**
*假设/data/data/com.example.myapp/files/images/abc.log文件存在
* 下面通过 FileProvider 分享 abc.log 文件
*/
// filesDir的值为/data/data/com.example.myapp/files/
File filesDir = getFilesDir();
// imagesDir的值为/data/data/com.example.myapp/files/images/
File imagesDir = new File(filesDir, "images");
//将要分享的abc.log创建一个对象
File logFile = new File(imagesDir, "abc.log");
private void shareLogFile(File logFile) {
// 通过 FileProvider 获取 content:// 形式的 Uri,最终logUri的值为content://com.example.myapp.fileprovider/log_images/abc.log
Uri logUri = FileProvider.getUriForFile(
this,
"com.example.myapp.fileprovider", // 与 manifest 中 authorities 一致
logFile
);
// 构建分享 Intent
Intent shareIntent = new Intent(Intent.ACTION_SEND);
// 日志文本,使用 text/plain;如果是二进制可用 application/octet-stream
shareIntent.setType("text/plain");
// 把文件 Uri 放到 EXTRA_STREAM 中
shareIntent.putExtra(Intent.EXTRA_STREAM, logUri);
// 非常重要:给目标 App 临时授予读取权限
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 弹系统分享面板,让用户选择微信/邮件等
startActivity(Intent.createChooser(shareIntent, "分享日志文件"));
}
}
五、FileProvider 的安全优势总结
| 能力 | file:// | FileProvider(content://) |
|---|---|---|
| 是否暴露真实路径 | ❌ 会 | ✅ 不会 |
| 能否遍历目录 | ❌ 会 | ✅ 无法 |
| 私有目录访问 | ❌ 泄露结构 | ✅ 隐藏结构 |
| Android 7.0+ 是否崩溃 | ❌ 会 | ✅ 不会 |
| 精确授权单文件 | ❌ 不支持 | ✅ 支持 |
| 推荐方式 | ❌ 禁止 | ✅ 官方推荐 |