Android 文件共享安全指南:为何不能使用 file://

56 阅读4分钟

引言

在 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:// 的信号。 image.png

三、错误示例(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:// URIcontent://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+ 是否崩溃❌ 会✅ 不会
精确授权单文件❌ 不支持✅ 支持
推荐方式❌ 禁止✅ 官方推荐