一、场景介绍
ContentProvider 是 Android 系统中用于跨应用数据共享的核心组件。
当应用需要对外共享文件时,通常会通过 ContentProvider 或 FileProvider 提供文件访问能力。
在 ContentProvider 中,系统定义了一个用于文件访问的标准接口:
public ParcelFileDescriptor openFile(
Uri uri,
String mode,
CancellationSignal signal
)
如果 应用导出的 ContentProvider 或 FileProvider 重写(override)了 openFile 方法, 并且 未对外部传入的 Uri 路径进行严格校验,则可能导致路径穿越(Path Traversal)漏洞,进而造成应用私有文件泄露。
二、风险边界说明(重要)
✅ 安全情况(无风险)
- 自定义的 ContentProvider / FileProvider 未重写 openFile() 方法*直接使用系统原生实现 👉 系统原生 ContentProvider / FileProvider 的 openFile 实现本身是安全的,
此情况下不存在路径穿越风险。
❌ 高风险情况
- 自定义 ContentProvider / FileProvider,且重写了 openFile() 方法,且实现方式不当(未校验路径合法性)
三、风险分析与典型漏洞案例
案例 1:直接使用****uri.getPath()拼接文件路径(最常见)
public class InsecureFileProvider extends ContentProvider {
@Override
public ParcelFileDescriptor openFile(
@NonNull Uri uri,
@NonNull String mode
) throws FileNotFoundException {
// ❌ 问题 1:未校验调用方权限
// ❌ 问题 2:直接使用 uri.getPath() 构造 File
File file = new File(uri.getPath());
int fileMode = ParcelFileDescriptor.MODE_READ_ONLY;
if ("w".equals(mode) || "wt".equals(mode)) {
fileMode = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
}
return ParcelFileDescriptor.open(file, fileMode);
}
}
攻击者可构造如下恶意 URI:content://com.demo.provider/../../data/data/com.demo.app/shared_prefs/token.xml
由于 uri.getPath() 返回的是未经校验的路径字符串,最终可能导致读取应用私有目录下的敏感文件。
案例 2:拼接“根目录 + 相对路径”,但未做 canonical 校验
public class InsecureRootFileProvider extends ContentProvider {
private File baseDir;
@Override
public boolean onCreate() {
// baseDir = /data/user/0/com.demo.app/files/export
baseDir = new File(getContext().getFilesDir(), "export");
return true;
}
@Override
public ParcelFileDescriptor openFile(
@NonNull Uri uri,
@NonNull String mode
) throws FileNotFoundException {
// ❌ 直接使用 getLastPathSegment,未做 canonicalPath 校验
String relativePath = uri.getLastPathSegment();
File target = new File(baseDir, relativePath);
return ParcelFileDescriptor.open(
target,
ParcelFileDescriptor.MODE_READ_ONLY
);
}
}
这里面, getLastPathSegment()存在安全陷阱。
getLastPathSegment() 的内部处理逻辑是:
- 先按 / 分段(此时尚未 URL Decode)****
- 对每个分段做 URL 解码****
- 返回最后一个分段作为文件名
如果攻击者将 / 编码为 %2F,就可以绕过分段逻辑。
攻击构造示例
1)攻击者构造整体编码路径
%2F..%2F..%2Fdata%2Fdata%2Fcom.demo.app%2Fshared_prefs%2Ftoken.xml
2)getPathSegments()处理过程
- 原始 path:
%2F..%2F..%2Fdata%2Fdata%2Fcom.demo.app%2Fshared_prefs%2Ftoken.xml - 按 / 分段:因为没有真实的 /,只得到 一个 segment
- URL Decode 后
/../../data/data/com.demo.app/shared_prefs/token.xml
3)最终路径拼接
File target = new File(
"/data/user/0/com.demo.app/files/export",
"/../../data/data/com.demo.app/shared_prefs/token.xml"
);
Java File 语义说明
- 如果第二个参数 以 / 开头 → 被视为绝对路径****
- 前面的 baseDir 会被完全忽略****
- 即使不是 / 开头,../ 也会在 canonical 化过程中跳出 baseDir
最终导致访问:/data/data/com.demo.app/shared_prefs/token.xml,应用私有文件泄露
五、安全建议(强烈建议)
1️⃣ 权限层面
- 尽量对 ContentProvider / FileProvider 设置访问权限
- 避免导出为任意应用可访问
- 优先使用 signature 或显式授权
2️⃣openFile()实现层面(核心)
无论 Uri 如何构造,最终访问文件的 canonicalPath 必须受控
安全原则:
实际访问路径(canonicalPath)必须位于允许的根目录(baseDir)之下****
案例 1 的安全写法
File baseDir = new File(getContext().getFilesDir(), "export");
File baseDir = new File(getContext().getFilesDir(), "export");
String path = uri.getPath();
if (path.startsWith("/")) {
path = path.substring(1);
}
File target = new File(baseDir, path);
// 核心校验:canonicalPath 必须在 baseDir 内
String base = baseDir.getCanonicalPath();
String real = target.getCanonicalPath();
if (!real.startsWith(base + File.separator)) {
throw new SecurityException("Path traversal detected");
}
return ParcelFileDescriptor.open(
target,
ParcelFileDescriptor.MODE_READ_ONLY
);
案例 2 的安全写法
File baseDir = new File(getContext().getFilesDir(), "export");
String path = uri.getLastPathSegment();
if (path == null) {
throw new FileNotFoundException();
}
File target = new File(baseDir, path);
// 核心校验:canonicalPath 必须在 baseDir 内
String base = baseDir.getCanonicalPath();
String real = target.getCanonicalPath();
if (!real.startsWith(base + File.separator)) {
throw new SecurityException("Path traversal detected");
}
return ParcelFileDescriptor.open(
target,
ParcelFileDescriptor.MODE_READ_ONLY
);
六、风险排查建议
-
在 AndroidManifest.xml 中定位自定义 provider
-
找到对应的实现类
-
检查是否重写了 openFile() 方法
- ❌ 未重写:不存在路径穿越风险****
- ⚠️ 已重写:重点审计路径拼接与 canonical 校验逻辑
-
对照本文风险分析逐项核查
七、参考资料
-
Google 官方安全说明